Python
Django

Django 2.0へのアップデート (Removed support for bytestrings編)

2017年12月にDjango 2.0がリリースされ、その後順調に修正を積み重ね、2018年5月15日現在、最新版の2.0.5がリリースされています。そのような中、多くのDjangoアプリケーション開発者はDjango 1.11 LTSで運用していたアプリケーションを2.0にアップデートするのではないかと思います。(既に完了している人が多いかと思いますが)

リリースノートはオリジナル以外にも日本語への翻訳もあります(が、残念ながらほとんど未翻訳です)。オリジナル(英語)を読むのがオススメですが、日本語でないとどうしても困る方には Django 2.0の変更点について - @massa142 などは役に立つと思います。

背景

Python 2.7.x/Django 1.9.xで開発キックオフしたが、Python 2.7.x=>3.5.xに変更した上で開発スタートし、テスト期間中にDjango 1.10.xに上げてサービス開始、途中、Python 3.6.x/3.6.x+1/Django 1.11.x/Django 1.11.x+1/Django 1.11.x+2...へアップデート実施済み。その間、アップデートで修正が必要だったものは ForeignKey on_delete のみだった。

Djangoでのセマンティックバージョニング採用後として初のリリースなので、ピニング戦略はこのタイミングで改めて考えるのがよいでしょう。2017年4月リリースのDjango 1.11.xのエクステンデッドサポートは2020年4月まで続くので、2019年4月にDjango 2.2 LTSが出てから考えるのがよいというところはある。

一方でDjango 1.11 LTSはPython 3.4, 3.5, 3.6しかサポートしておらず、まもなくリリースのPython 3.7が利用できない(自分でDjango 1.11 x Python 3.7を保守してもいいでしょうけど)。そのような理由でPython 3.7の機能/非機能が必要であるため、留まることはできないので、今回のケースDjango 2.x系への移行は事実上必須となりました。

ちなみに、Python 3.7の新機能についてはPython3.7の新機能 by ksato9700が詳しいです。

今回も問題なく、Django 1.11.x->2.0.xのアップデートは完了するかに見えたのですが、以下の問題が1点ありました。

Removed support for bytestrings in some places

Removed support for bytestrings in some places

To support native Python 2 strings, older Django versions had to accept both bytestrings and unicode strings. Now that Python 2 support is dropped, bytestrings should only be encountered around input/output boundaries (handling of binary fields or HTTP streams, for example). You might have to update your code to limit bytestring usage to a minimum, as Django no longer accepts bytestrings in certain code paths. Python’s -b option may help detect that mistake in your code.

For example, reverse() now uses str() instead of force_text() to coerce the args and kwargs it receives, prior to their placement in the URL. For bytestrings, this creates a string with an undesired b prefix as well as additional quotes (str(b'foo') is "b'foo'"). To adapt, call decode() on the bytestring before passing it to reverse().

https://docs.djangoproject.com/en/2.0/releases/2.0/#removed-support-for-bytestrings-in-some-places

Django 2.0ではPython 2を完全にドロップすることができるので、bytes (bytestring) のサポートを切るというものでした。

"some places"とあったので、具体的にどこで適用されるのかを確認せず、スルーしていました。

結果

>>> from polls.models import Choice
>>> bytes_text = some_func() # => b'Just hacking'
>>> Choice(choice_text=str(bytes_text)).save()
>>> Choice.objects.filter(choice_text=bytes_text) # => NG
<QuerySet []>
>>> Choice.objects.filter(choice_text=bytes_text.decode()) # => OK
<QuerySet [<Choice: Just hacking>]>

bytesでI/Fされている関数からの返り値を気にせず、そのままDjangoに渡してしまっています。

予防策1

bytesでI/Fされている部分が残っていることが問題でした。それぞれのコンポーネントの担当者が別々であると起こりやすいかもしれません。同一人物が担当していたとしても、長期で運用しているサービスだと起こる可能性が高そうです。
このように別々のコンポーネントをPython内部で結合せず、マイクロサービスとして分割してやり、各サービスごとにgRPCなどで連携してやる必要があるでしょう。

マイクロサービス(とそれを支える仕組みであるサービスメッシュ)はバズワードとして嫌われがちですが、このような点からも実務レベルで導入する価値のあるコンセプトだと理解できます。

予防策2

bytesをDjango側に渡しても、基本的にはWarningやErrorにはならないようです。そこで、CI/CDレベルで以下の対応をしておく必要がありそうです(Command line and environment)。

python manage.py test

とあるところで

python -bb manage.py test

としておくべきというものです。

なお、このオプションは環境変数での実装がないようなので、pytestを使う場合はどうするといいか分からないので、知っていたら教えてください。

まとめ

Django 1.11から2.0へのアップデートで久しぶりに手間取ったので、そのケースを少し共有しました。Djangoは長期間にわたりメンテナンスする必要があるシステムにおいてバージョンを上げ続けていても、ほとんど手直しをする必要がないので、その意味でよいフレームワークです。

以下の通り、Django関連のカンファレンスが続くので、2018年のDjangoも目が離せないようです。

また、わざわざカンファレンスに参加しなくてもDjangoをよくするコントリビュートはできるようです。つい先週Djangoにコードコントリビューションする機会があったのですが、本家のコントリビューションガイドはとても充実していました。またDjangoドキュメント 日本語翻訳 Dayのようなイベントも開催していますので、ぜひご参加ください。

参考