Python3.7 & Django 2.1 な個人アプリを Python 3.10 & Django 4.1 へとアップデートした

前回の記事で環境を構築したアプリですが、動作環境は Python 3.7 & Django 2.1 でした。
Python3.4 & Django1.8な個人アプリを、Python3.7 & Django 2.1 へとアップデートした - メモ的な思考的な

また、ブログには書いていませんでしたが、その後 Python 3.8 系とDjango 2.2 系にバージョンアップしていました。
https://github.com/thinkAmi/dj_ringo_tabetter/commit/236895fb8a7e85a6f46ec7e3555bb400354438f1

 
今回、WSL2 Ubuntu 環境にて Djangoアプリを最新の Python 3.10 & Django 4.1 へバージョンアップしたため、メモを残します。

 
目次

 

環境

 

バージョンアップ方針

やること

基本的にはテストがパスして動作すればOKとします。

なお、Djangoの公式ドキュメントによると、Django 4.1系はPython3.8系をサポートしているため、

  • まずはDjangoまわりをバージョンアップ
    • pip-review で一気にバージョンアップ
      • 小さなアプリなため、Django3系を飛ばしてもある程度は動くはず...
  • 最後にPythonをバージョンアップ

の流れで実施することにします。

なお、バージョンアップする上で問題が発生した場合は、都度対応していきます。

 

やらないこと

パッケージ管理方法の変更

今は rquirements.txt で各パッケージのバージョンを管理しています。

将来的には何かしらに移行したいですが、今回はこのままにします。

 

機能追加

今回はバージョンアップだけにします。

 

バージョンアップ開始

pip-reviewによるバージョンアップ

前回の記事でDjangoが動くところまで確認しているため、 pip-review を使って一気にパッケージをバージョンアップします。
jgonggrijp/pip-review: A tool to keep track of your Python package updates.

 

importlib-metadata のエラーに対応

pip-reviewを実行したところ、 importlib-metadata のバージョンアップでエラーが出ました。

$ pip-review --auto
Collecting atomicwrites==1.4.1
  Using cached atomicwrites-1.4.1.tar.gz (14 kB)
  Preparing metadata (setup.py) ... error
  error: subprocess-exited-with-error
  
  × python setup.py egg_info did not run successfully.
  │ exit code: 1
  ╰─> [25 lines of output]
      /home/<user>/dev/projects/dj_ringo_tabetter/env/lib/python3.8/site-packages/setuptools/_importlib.py:23: UserWarning: `importlib-metadata` version is incompatible with `setuptools`.
      This problem is likely to be solved by installing an updated version of `importlib-metadata`.
        warnings.warn(msg)  # Ensure a descriptive message is shown.

 
同じようなエラーがないかを探したところ、 setuptools に issue がありました。
[BUG] ImportError when using importlib with setuptools 60.9.0+ · Issue #3292 · pypa/setuptools

 
そこで、 requirements.txt にある importlib-metadata のバージョンを 0.20 から 0.21 へと修正します。

importlib-metadata==0.21

 
続いて、 pip install -r requirements.txt して、現在の環境の importlib-metadata をバージョンアップします。

バージョンアップ後、 pip-review --auto したところ、エラーとなることなくバージョンアップが完了しました。

 

Djangoアプリのテストを実行

PostgreSQLに関するエラーが出て失敗

Djangoアプリには pytest のテストコードがあるため、実行してみました。

すると

$ python -m pytest
...
django.db.utils.NotSupportedError: PostgreSQL 11 or later is required (found 10.6).

のような PostgreSQLのバージョンに関するエラーが発生しました。

公式ドキュメントを見ると、Django 4.1系でPostgreSQL 10 系のサポートがなくなったために発生しているようでした。
SupportedDatabaseVersions – Django

 
PostgreSQLが今のままでは動作しないため、対応を考えました。案としては以下のとおりです。

  • PostgreSQLのバージョンを上げる
  • 別のDBに移行する

 
PostgreSQLのバージョンを上げる場合、Djangoアプリの構成を変更せずに済みそうです。

ただ、現在の開発環境だとPostgreSQLはDockerで構築しているため、アップグレード作業が必要そうでした。

アップグレードの方法は色々ありそうでした。

 
一方、PostgreSQLからSQLiteへ移行する場合、Djangoの機能だけでできそうでした。
DjangoをsqliteからPostgreSQLに切り替えた(dumpdata/loaddata) - [Dd]enzow(ill)? with DB and Python

 
そこで今回は、 PostgreSQL から SQLite へ移行することにしました。

 

DBをPostgreSQLからSQLiteへ移行

再掲となりますが、以下の記事を参考に移行を進めます。
DjangoをsqliteからPostgreSQLに切り替えた(dumpdata/loaddata) - [Dd]enzow(ill)? with DB and Python

 
以下のコマンドを実行し、PostgreSQLのデータをダンプします。

$ python manage.py dumpdata --exclude auth.permission --exclude contenttypes > dump.json

 
settings.pyをSQLiteの設定へと書き換えます。

DATABASES = {
    'default': {
        'NAME': os.path.join(BASE_DIR, 'ringo.db'),
        'ENGINE': 'django.db.backends.sqlite3',
    }

 
データベースのマイグレーションをします。

$ python manage.py migrate

 
データをロードします。

$ python manage.py loaddata dump.json

Installed 794 object(s) from 1 fixture(s)

 
PyCharm の Database で中身を確認すると、データが移行されていました。

 
その後、Djangoアプリを起動して確認すると円グラフが表示されました。移行は成功したようです。

 

PostgreSQLの関数を使っているコードを修正

月別の折れ線グラフ用のデータを作成するために、Djangoアプリでは以下のコードを書いていました。

ここの extra() で使っている date_partPostgreSQLの関数になります。
9.9. 日付/時刻関数と演算子

cls.objects.extra(select={'month': "date_part('month', tweeted_at)::int"}) \
                .values('name', 'month') \
                .annotate(quantity=models.Count('name')) \
                .order_by('name', 'month')

 
このままではSQLiteで動作しないため、SQLite用の関数に修正します。

SQLiteでは strftime 関数を使うことで、同じような挙動が実現できそうでした。
Date And Time Functions

 
ただ、上記の公式ドキュメントの例では、strftimeに列名を渡している例が見つかりませんでした。

そこで stackoverflowの記事を探したところ、以下で列名を strftime を渡しているものがありました。 strftime の第二引数に列名を ' 無しで渡せば良いようです。
strftime function in SQLite does not work - Stack Overflow

そこで、stackoverflowを参考に修正します。

cls.objects.extra(select={'month': "strftime('%m', tweeted_at)"}) \
    .values('name', 'month') \
    .annotate(quantity=models.Count('name')) \
    .order_by('name', 'month')

 
差し替えた後、Djangoアプリの月別折れ線グラフがエラーにならないか確認しましたが、問題なく表示できました。

ただ、テストコードが落ちるようになりました。戻り値の配列が PostgreSQLSQLite では異なっているのが原因だったため、テストコードも修正しました。

 

Djangoの警告に対応

ここまででDjangoは起動できるようになりましたが、 run server したときに警告が出てましたので、対応します。

 

DEFAULT_AUTO_FIELD の警告に対応

run sever したときに以下の警告が出ていました。

WARNINGS:
tweets.LastSearch: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
    HINT: Configure the DEFAULT_AUTO_FIELD setting or the TweetsConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
tweets.Tweets: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
    HINT: Configure the DEFAULT_AUTO_FIELD setting or the TweetsConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.

System check identified 2 issues (0 silenced).

 
この警告を調べたところ、以下の記事がありました。Djagno 3.2の変更によるものでした。

 
主キーを django.db.models.AutoField のままにするか django.db.models.BigAutoField にするか考えましたが、桁数が多いほうが良いだろうと考え django.db.models.BigAutoField とすることにしました。
Model field reference | Django documentation | Django

そこで、 settings.py の末尾に以下を追加しました。

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

 
変更後マイグレーションが必要なので実行します。

# マイグレーションファイルを作成
$ python manage.py makemigrations
Migrations for 'tweets':
  apps/tweets/migrations/0002_alter_lastsearch_id_alter_tweets_id.py
    - Alter field id on lastsearch
    - Alter field id on tweets

# マイグレーションを適用
$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, tweets
Running migrations:
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying tweets.0002_alter_lastsearch_id_alter_tweets_id... OK

 
Djangoアプリを再起動したところ、警告は出なくなりました。

 

Python環境を3.10.7へバージョンアップ

前回の記事で pyenv を使ってPythonを入れていたため、今回も pyenv を使って Python 3.10.7 をインストールします。

Pythonのバージョンアップ

# venvを無効化
(env) $ deactivate

# pyenvでPython3.10.7をインストール
$ pyenv install 3.10.7
Downloading Python-3.10.7.tar.xz...
-> https://www.python.org/ftp/python/3.10.7/Python-3.10.7.tar.xz
Installing Python-3.10.7...
Installed Python-3.10.7 to /home/<user>/.anyenv/envs/pyenv/versions/3.10.7

# pyenvでバージョン切り替え
$ pyenv local 3.10.7

# バージョン確認
$ python --version
Python 3.10.7

# venv環境の作成
$ python -m venv env_3_10_7

# 有効化
$ source env_3_10_7/bin/activate

 

パッケージの再インストール

requirements.txt からパッケージを再インストールします。

 

backports.zoneinfo のエラーに対応

再インストールしたところ、以下のエラーが発生しました。

(env_3_10_7) $ pip install -r requirements.txt

...
  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for backports.zoneinfo
Failed to build backports.zoneinfo

 
requirements.txt を見ると、たしかに backports.zoneinfo がありました。

backports.zoneinfo==0.2.1

 
zoneinfo モジュールですが、Python 3.9から標準パッケージとして導入されました。
Python 3.9の新機能 - python.jp

そのため、 backports.zoneinfoPython 3.9 未満向けのバックポートパッケージと考えられました。
pganssle/zoneinfo: Reference implementation for the proposed standard library module zoneinfo

 
今回構築するPython3.10系の環境では不要と判断し、 requiremetns.txt から backports.zoneinfo の行を削除しました。

再度 pip install -r requirements.txt すると、エラーにならずにインストールが完了しました。

 

動作確認

再度

  • Djangoアプリの起動
  • python -m pytest によるテストの実行

を行い、エラーなく動作していることが確認できました。

 
これにより、Python 3.10 & Django 4.1 へのバージョンアップが完了しました。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi/dj_ringo_tabetter

今回のプルリクはこちら。
https://github.com/thinkAmi/dj_ringo_tabetter/pull/16

なお、Herokuへアップロードしている master ブランチへはプルリクをマージせず、しばらくは development ブランチへマージすることにします。