Django: sessionの有効期限を設定するset_expiry()は、引数が0の時には有効期限を変更しない


# 概要と結論
タイトルの通りですが、しばらくハマっていたのでメモを残しておきます。

結論としては、`set_expiry(0)`とした時には有効期限=0となるのではなく、ブラウザを閉じた瞬間にSessionが破棄されます。もちろん挙動としては有効期限を０にしても同じなのですが、例えばTestCaseを書く時、sessionの有効期限を取得する`get_expiry_age()`を呼ぶと困ります。**0ではなく、デフォルトの有効期限(1209600 = 2週間の秒表記)が戻ってくる**からです。

# 前提知識: Djangoのsession

[Django公式ドキュメント: セッションの使い方](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/)
こちらのリンクに書かれている通り、デフォルトの設定でDjangoはCookieにSessionIDを保存し、それを利用してサーバ側のデータベースに、訪問者ごとのデータを保存しています。
Requestオブジェクトにはsession属性があり、viewなどからこちらにデータを保存or読み取りできます。↑のリンクの [この辺](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/#session-object-guidelines) をみると、具体的な使い方が書いてあります。
また、**ユーザのログイン情報**もsessionを利用して保持しています。


# Sessionの有効期限

sessionには**有効期限**が存在していて、デフォルトでは2週間データを保持します。これはsettingsの**SESSION_COOKIE_AGE**変数によって変更できます。[公式ドキュメントのこの部分に記載があります。](https://docs.djangoproject.com/ja/3.0/ref/settings/#session-cookie-age)

また、viewなどからsessionの有効期限を設定することができます。上述のRequestオブジェクトのsession属性から、`set_expiry`メソッドを呼べばいいです。


# 本題: set_expiry(0)の意味

set_expiry(0)の挙動は[Django公式ドキュメント: セッションの使い方: set_expiry](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry) こちらに記載されています。以下に引用します。

>もし value が 0 ならば、ユーザのセッションクッキーは、ユーザがウェブブラウザを閉じた時に破棄されます。

なので、以下のようになります。

```python

# 1(0以外)をセットした場合
>>>request.session.set_expiry(1)
>>>rquest.session.get_expiry_age()   # 有効期限を取得するメソッド
1   # 設定した値が取得できる

>>>request.session.get_expire_at_browser_close()  # ブラウザを閉じた時にセッションクッキーを破棄するかを調べるメソッド。
False  # 破棄しない




# 0をセットした場合
>>>request.session.set_expiry(0)
>>>rquest.session.get_expiry_age()
1209600    # 0ではなく、デフォルト値が表示される！(これはsettings.SESSION_COOKIE_AGE由来)

>>>request.session.get_expire_at_browser_close()
True  # Trueを返すようになる(破棄する)
```

直感的には`set_expiry(0)`の例で、`get_expiry_age()`を呼べば0が帰ってくるように思います。しかし実際は0をセットした時、有効期限は変更されません。

代わりと言っては何ですが、「ブラウザを閉じた時にセッションクッキーを破棄するか？」を調べることができます。
それが`get_expire_at_browse_close()`メソッドです。

セッションの有効期限(get_expiry_ageで取得できる値)は、あくまで**ブラウザを閉じてもセッションクッキーを残す**という条件でのみ意味を成します。


# 余談

ソースコードを見てみましょう。(やり方が正しいかは不明...)

まずDjangoのrootディレクトリを調べて、cdコマンドで移動しておきます。

```bash
$ python -c 'import django; print(django.__path__)'
['/Users/kyu/Document/venv/lib/python3.7/site-packages/django']

# 移動
$cd /Users/kyu/Document/venv/lib/python3.7/site-packages/django
```


先ほどの公式ドキュメントの [ここ](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase) をみると、set_expiryやget_expiry_ageは ` backends.base.SessionBase`というクラスに定義されているようです。なので、以下のようにして探します。

```bash
# rはリカーシブです。
# lはファイル名だけ表示するオプションです。つけないと、本文も隣に出てきて見づらいです....
$ grep 'SessionBase' -rl .  # 現在の場所(.)の下からSessionBaseがテキスト内部に含まれるfileを探します。

....省略.....
# 見つかりました！
./contrib/sessions/backends/base.py
```

開いて中を見てみると....


```python:backends/base.py

# まずはset_expiryを見てみます。

258     def set_expiry(self, value):
# ....省略.............
# 最終的に、_session_expiry に保存しているようです。
282     ¦   self['_session_expiry'] = value
```

次はgetメソッドです。

```python:backends/base.py

212     def get_expiry_age(self, **kwargs):
# ............省略..................
225     ¦   try:
226     ¦   ¦   expiry = kwargs['expiry']
227     ¦   except KeyError:
228     ¦   ¦   expiry = self.get('_session_expiry')
# ............省略..................
230     ¦   if not expiry:   # Checks both None and 0 cases
231     ¦   ¦   return self.get_session_cookie_age()
232     ¦   if not isinstance(expiry, datetime):
233     ¦   ¦   return expiry
```

kwargsを指定しなければ、先ほど保存された`_session_expiry`由来の値がexpiry変数に束縛されているはずです。

興味深いのは230~231行目です。コメントも書かれていますが、**0はFalse扱いなので、set_expiryで０をセットした場合には`get_session_cookie_age()`の値が帰ります。** 
これが本記事で取り上げた挙動の原因です。(expiryの値は返らない)

正直、ゼロをセットしたのならゼロを返せばいいような気もしますが、ドキュメントにもソースコードのコメントにもこのように書かれているからには何か意味があるのでしょう....(私にはわかりませんでした)