Python 2.7で稼働中のDjangoアプリケーションをPython 3.5へ移行する

## これは何？
このドキュメントは[Python 2.7で稼働しているDjangoアプリケーション](https://ticketcamp.net/)をPython 3.5に移行するコストを調査した際のメモです。

## 2to3
[`xrange`の変更](http://diveintopython3-ja.rdy.jp/porting-code-to-python-3-with-2to3.html#xrange)や[`keys()`, `values()`, `items()`の変更](http://diveintopython3-ja.rdy.jp/porting-code-to-python-3-with-2to3.html#dict)を`2to3`で変換する。

`-x`で適用しない変換を指定可能なので、以下の変換は除外した。

- `future`
  - `from __future__ import unicode_literals`は残っていても支障がないのでdiffを少なくするために除外
- `print`
  - `print`関数を使うように徹底してきて、`print`文は存在しないはずなので除外
- `callable`
  - `callable`が利用できないのはPython 3.0/3.1だけであり、Python 3.5.xへの移行を想定しているため不要。

`-w`オプションで元のファイルを上書きして変換を実行。

```shell-session
$ 2to3 -x future -x print -x callable -w .
```

## アップデートが必要だったライブラリ
### python-memcached
- https://pypi.python.org/pypi/python-memcached

だだしバイナリをMemcachedに入れた時の挙動は怪しいと思う。

### tweepy
- https://pypi.python.org/pypi/tweepy

### google-api-python-client
- https://github.com/google/google-api-python-client

### Django
1.8.xにアップデートが必要。

```
File "/home/csakatoku/.pyenv/versions/3.5.0/lib/python3.5/site-packages/django/utils/html_parser.py", line 12, in <module>
    HTMLParseError = _html_parser.HTMLParseError
AttributeError: module 'html.parser' has no attribute 'HTMLParseError'
```

## 乗り換えが必要なライブラリ
### dnspython
別のパッケージ名でリリースされている[`dnspython3`](https://pypi.python.org/pypi/dnspython3)に変更する必要がある。`pip`でインストールするときに`dnspython3`を指定すればOKで、コードの変更は不要。

### MySQL-python
- https://pypi.python.org/pypi/MySQL-python
  - `MySQL-python`ではなく[`mysqlclient`](https://pypi.python.org/pypi/mysqlclient)を使えば問題なし。

## 動作しないライブラリ
### bitly-api
`ImportError`で動作しない。ただし[master](https://github.com/bitly/bitly-api-python)では修正されている模様。

```
>>> import bitly_api
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/csakatoku/.pyenv/versions/3.5.0/lib/python3.5/site-packages/bitly_api/__init__.py", line 1, in <module>
    from bitly_api import Connection, BitlyError, Error
ImportError: cannot import name 'Connection'
```

## django_ses
[`try`...`except`文](http://diveintopython3-ja.rdy.jp/porting-code-to-python-3-with-2to3.html#except)の問題でインポートしようとした瞬間に落ちる。ただしmasterでは修正されている模様。

```
>>> import django_ses
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/csakatoku/.pyenv/versions/3.5.0/lib/python3.5/site-packages/django_ses/__init__.py", line 173
    except SESConnection.ResponseError, err:
                                      ^
SyntaxError: invalid syntax
```

## pynliner
`print`文による`SyntaxError`。[Python 3対応のPull Request](https://github.com/rennat/pynliner/pull/41)が出されている。

```
Collecting pynliner
  Using cached pynliner-0.5.2.tar.gz
Collecting BeautifulSoup<4.0,>=3.2.1 (from pynliner)
  Using cached BeautifulSoup-3.2.1.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 20, in <module>
      File "/tmp/pip-build-7je9i10s/BeautifulSoup/setup.py", line 22
        print "Unit tests have failed!"
                                      ^
    SyntaxError: Missing parentheses in call to 'print'
```

## 修正が必要なコード
### hashlib.sha1の引数には`binary`を渡さないといけない

Bad

```
>>> import hashlib
>>> hashlib.sha1("SPAM").hexdigest()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Unicode-objects must be encoded before hashing
>>> hashlib.sha1("SPAM").hexdigest()
```

Good

```
>>> import hashlib
>>> hashlib.sha1("spam".encode("utf-8")).hexdigest()
'ded982e702e07bb7b6effafdc353db3fe172c83f'
```

Good

```
>>> import hashlib
>>> hashlib.sha1(b"spam").hexdigest()
'ded982e702e07bb7b6effafdc353db3fe172c83f'
```

## urllib.basejoinがなくなった

どこに行った？

## `sort`の引数が変わった
- https://docs.python.org/3.5/howto/sorting.html?highlight=sort#the-old-way-using-the-cmp-parameter

Bad

```
>>> data = [{"value": 1}, {"value": 3}, {"value": 2}]
>>> data.sort(lambda a, b: cmp(a["value"], b["value"]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must use keyword argument for key function
```

Good

```
>>> data = [{"value": 1}, {"value": 3}, {"value": 2}]
>>> data.sort(key=lambda x: x["value"])
>>> print(data)
[{'value': 1}, {'value': 2}, {'value': 3}]
```

この変更が一番きつい。