Python2系でもPython3系でも簡易Web鯖をワンライナーで立てる

背景

jsakamotoさんのMarkdownPresenterにて、


Update readme.md to work with current version of python by AlanKavanagh · Pull Request #8 · jsakamoto/MarkdownPresenter · GitHub
ということに首を突っ込んでPython2系でもPython3系でも動くように模索しました。


↓発端(英語はワカリマセン)

目的

とりあえず、Pythonのバージョンごとで実行するならば

  • Python2系
$ python -m SimpleHTTPServer
  • Python3系
$ python -m http.server


で実行できる事を、ワンライナーで両対応させたいとのこと(だと思った)。

試行 その1


ちょこちょこ調べることに、単純にバージョンで切り替える場合、
sys.version_infoが使える。

import sys
if sys.version_info < (3, 0, 0):
    #Python2系
    import SimpleHTTPServer as server
else:
    #Python3系
    import http.server as server

server.test()


これを元にワンライナーしていこうとした。
そこで取り出したるは__import__関数

$ python -c 'import sys;__import__("SimpleHTTPServer"if sys.version_info<(3,0,0)else"http.server").test()'


コレで楽勝!…というわけにも行かず、
Python3の時に、test関数なんてないよ!って言われる。

Traceback (most recent call last):
  File "<string>", line 1, in <module>
AttributeError: 'module' object has no attribute 'test'


原因解決するために対話モードで実行

$ python
Python 3.3.0 (default, Jan  8 2014, 13:37:36)
[GCC 4.1.2 20080704 (Red Hat 4.1.2-52)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> m = __import__("http.server")
>>> dir(m)
['__builtins__', '__cached__', '__doc__', '__file__', '__initializing__', '__loader__', '__name__', '__package__', '__path__', 'client', 'server']
>>> dir(m.server)
['BaseHTTPRequestHandler', 'CGIHTTPRequestHandler', 'DEFAULT_ERROR_CONTENT_TYPE', 'DEFAULT_ERROR_MESSAGE', 'HTTPServer', 'SimpleHTTPRequestHandler', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__initializing__', '__loader__', '__name__', '__package__', '__version__', '_quote_html', '_url_collapse_path', 'argparse', 'copy', 'email', 'executable', 'html', 'http', 'io', 'mimetypes', 'nobody', 'nobody_uid', 'os', 'posixpath', 'select', 'shutil', 'socket', 'socketserver', 'sys', 'test', 'time', 'urllib']

__import__関数だと深い階層までimportしないらしく、上記の対話モードでも

m = __import__("http.server")

とimport指定してるにもかかわらず、dir(m)にてserverが出てきてしまっている。
__import__が一番浅い所(この場合、http)までしか、importできてない。
もう少し調べたら、ワンライナー用にこんな書き方があるぐらいなので、
多分短く書く手法としてはコレ以上やるのは無理そう。

試行 その2


次に2系3系のどっちかなのでSimpleHTTPServerがimportエラー出た時、http.serverインポートすればいいや作戦。


ベースはこんな感じ

try:
    import SimpleHTTPServer as server
except:
    import http.server as server
server.test()


しかしtry-except句を1行でかくとこんなエラーが

>>> try:import SimpleHTTPServer as server except:import http.server as server
  File "<stdin>", line 1
    try:import SimpleHTTPServer as server except:import http.server as server
                                               ^
SyntaxError: invalid syntax

と、怒られる。
構文的にtry-except句は

try:import SimpleHTTPServer as server
except:import http.server as server

ぐらいしかコンパイラさんは認めてくれない。
ここで取り出したるは「exec」。
execの使用例はさすがは天下(?)のstackoverflowさん。
try-exceptをワンライナーで書くノウハウも有りました。

exec("try:import SimpleHTTPServer as server\nexcept:import http.server as server")


これできちんと動きます。

  • Python2系
$ python
Python 2.4.3 (#1, Feb  5 2013, 15:50:22)
[GCC 4.1.2 20080704 (Red Hat 4.1.2-52)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> exec("try:import SimpleHTTPServer as server\nexcept:import http.server as server")
>>>
  • Python3系
python
Python 3.3.0 (default, Jan  8 2014, 13:37:36)
[GCC 4.1.2 20080704 (Red Hat 4.1.2-52)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exec("try:import SimpleHTTPServer as server\nexcept:import http.server as server")
>>>


と、両環境でも動いたので、これを元に書いたワンライナーがこちら

$ python -c 'exec("try:import SimpleHTTPServer as m\nexcept:import http.server as m");m.test()'

Web鯖も起動したしこれにて終了!めでたしめでたし!!

新規問題(StatusCode 501問題)


…かと、おもいきやまだ問題が残ってた。
こちらの話だとGETメソッドの処理がなくて、501が出るとのこと。
確認してみた。

試行 その3

  • server側
$ python -V
Python 3.3.0
$ python -c 'exec("try:import SimpleHTTPServer as m\nexcept:import http.server as m");m.test()'
Serving HTTP on 0.0.0.0 port 8000 ...
127.0.0.1 - - [09/Jan/2014 12:10:42] code 501, message Unsupported method ('GET')
127.0.0.1 - - [09/Jan/2014 12:10:42] "GET /Presenter.html HTTP/1.1" 501 -
  • client側
$ nc localhost 8000
GET /Presenter.html HTTP/1.1

HTTP/1.0 501 Unsupported method ('GET')
Server: BaseHTTP/0.6 Python/3.3.0
Date: Thu, 09 Jan 2014 05:20:32 GMT
Content-Type: text/html;charset=utf-8
Connection: close

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 501</p>
        <p>Message: Unsupported method ('GET').</p>
        <p>Error code explanation: 501 - Server does not support this operation.</p>
    </body>
</html>

改善


Python2系では出なかったけど、Python3系で出ました。
なぜだろうと思ってライブラリの中身を見たら、
SimpleHTTPServer.py のtest関数とhttp/server.py のtest関数のデフォルト引数が若干変わってました。

  • SimpleHTTPServer.py
def test(HandlerClass = SimpleHTTPRequestHandler,
         ServerClass = BaseHTTPServer.HTTPServer):
  • http/server.py
def test(HandlerClass = BaseHTTPRequestHandler,
         ServerClass = HTTPServer, protocol="HTTP/1.0", port=8000):


主に「HandlerClass = SimpleHTTPRequestHandler」の部分が、
「HandlerClass = BaseHTTPRequestHandler」になっていたため、
do_ + HTTP method名の関数がHandlerクラスに定義されていないと501を返すようになっています。
そこで、test関数の引数でハンドラーを指定して対処します。
次のように改良しました。

結果

$ python -c 'exec("try:import SimpleHTTPServer as m\nexcept:import http.server as m");m.test(HandlerClass=m.SimpleHTTPRequestHandler)'


これで正常に動くのを確認しました。
多分、これで大丈夫(棒読)

おまけ


2to3使ってコンバートしたらどうなるかなぁという実験

# -*- encoding: utf8 -*-
import SimpleHTTPServer

SimpleHTTPServer.test()
$ 2to3 hoge.py
RefactoringTool: Skipping implicit fixer: buffer
RefactoringTool: Skipping implicit fixer: idioms
RefactoringTool: Skipping implicit fixer: set_literal
RefactoringTool: Skipping implicit fixer: ws_comma
RefactoringTool: Refactored hoge.py
--- hoge.py     (original)
+++ hoge.py     (refactored)
@@ -1,7 +1,7 @@
 # -*- encoding: utf8 -*-
-import SimpleHTTPServer
+import http.server

-SimpleHTTPServer.test()
+http.server.test()



RefactoringTool: Files that need to be modified:
RefactoringTool: hoge.py


引数まで関与してくれないので、
わざわざtest関数使って実装してる人は少ないとおもいますが、
地味な嵌まりどころかも知れません。