matobaの備忘録

育児しながら働くあるエンジニアの記録

pytestでデフォルトはスキップのテストを作る

テストケースを書いておきたいが、常にテストしたくないこともある。 特に非常にテスト実行が重いケース。

前提として、pytestのマーカー

pytestは、マーカーという概念がある。 マーカーという概念を使えば、テストケースに印を付けられる。 そして、特定のマーカーのテストだけを実行したり、特定のマーカーのテストを除外できる。

import pytest

@pytest.mark.hogehoge
def test_sample():
    pass

除外するときはこう実行する。

pytest -m "not hogehoge"

hogehoge の部分は、基本的に任意の文字列を指定できるはず。(すでに何かに使われている場合は誤作動が起きそうだが)

pytestのデフォルトオプションの指定

pytestでは、デフォルトのオプションを設定できる

私は、設定は pyproject.toml に書いている。その場合は以下のように書く。

[tool.pytest.ini_options]
addopts = "-m 'not hogehoge'"

これでデフォルトはスキップできる。

なお、markerの説明を設定に書いておかないと警告が出る。それも書いておくと以下のようになる。

[tool.pytest.ini_options]
addopts = "-m 'not hogehoge'"
markers = [
    "hogehoge: hoge test",
]

Pythonを使って、macOSのファイル作成日時を取得する

macOSのマシンを使っている。 ローカルのファイルを整理するスクリプトを書いていて、ファイルの作成日時を取得した時の話をする。

ローカルに大量の作業用ファイルがある

ある。作業用ファイルといってもいろんな種類があるが。 直近で言えば、私は考え事を整理するときにXmindを使っているのだが、XMindのファイルがたくさんある。 そして普通に多いので、整理したい。

「作成日付ごとにディレクトリを切って、そこに入れておきたいな」と思った。

Finderだとファイルの作成日時がある

知っての通り、Finderで見ると、ファイルにはファイルの作成日時が表示されている。 だから、行けるはずだろうと持ってスクリプトを書いてみた。Pythonである。

(「書いてみた」と言ったが、実際はClaudeに指示を出してスクリプトを出力し動作確認していた。)

しかし、何かうまくいかない。特定のファイルがFinderの作成日時と違うディレクトリに移動される。

os.path.getctimeを使っていた

そのスクリプトではファイルの作成日時を取得するために、Pythonのos.path.getctimeを確認していた。

docs.python.org

これが間違っている。

システムの ctime、Unix系など一部のシステムでは最後にメタデータが変更された時刻、Windows などその他のシステムでは path の作成時刻を返します。返り値はエポック (time モジュールを参照) からの経過時間を示す秒数になります。ファイルが存在しない、あるいはアクセスできなかった場合は OSError を送出します。

ctimeはUNIX系のシステムのファイルシステムの情報であるが、ファイルの作成日時ではない。ファイルシステムのメタデータの変更日時である。

じゃあ、どうすればいいのか?

ファイルシステムのメタデータの変更日時ではなくて、macOSのFinderで表示されている情報が欲しい。 と言うわけで調べてみた。

どうやらmacOSのファイルシステムに拡張メタデータがあり、それを見れば良さそうだ。

stackoverflow.com

Pythonで取得する方法を探す

上記のスタックオーバーフローでは、コマンドラインから取得している。 が、こう言うのはたいていライブラリがある。と思って探してみた。

github.com

あった。

さらにそこから、以下のライブラリを紹介されていた。

github.com

これが良さそうに見える。あとは、属性名が分かれば取得できそうだ。

必要な属性を探す

macOSの開発者ドキュメントを見ながら、ファイルのメタデータで、欲しいものを探す。

developer.apple.com

kMDItemContentCreationDate がそれっぽい。

developer.apple.com

Python経由で取得する

from osxmetadata import OSXMetaData, kMDItemContentCreationDate

def get_creation_date(file_path: str)-> datetime.datetime:
    md = OSXMetaData(file_path)
    creation_date = md.get(kMDItemContentCreationDate)
    return creation_date

できた。

めでたしめでたし。

Djangoのtimezone.nowに関するモヤモヤを調べる

Djangoのtimezone.nowを使っていてモヤモヤした。特に、「なぜ、UTCの時刻を返すのか」と思ったので、調べてみた。 その記録。

背景

Djangoにはtimezoneのutilsがあり、nowという関数を持っている。 これを実行すると次のようになる。

>>> from django.utils import timezone
>>> timezone.now()
datetime.datetime(2024, 3, 27, 20, 49, 31, 114299, tzinfo=datetime.timezone.utc)

また、Djangoプロジェクトにはsettingsでタイムゾーンが設定できる。 例えば次のような感じ。

TIME_ZONE = "Asia/Tokyo"

しかし、タイムゾーンを設定していても timezone.now() はUTC時刻を返す。 設定したタイムゾーンでの時刻にする場合は、 timezone.localtime() を使えることを知っていた。 なので、次に私はこんな風に書いてみた。

>>> timezone.localtime(timezone.now())
datetime.datetime(2024, 3, 28, 5, 55, 30, 810238, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))

めんどくさい。というか、私は何度も timezone.now() で設定したタイムゾーンの時刻が返ってくると勘違いしている。

「あれ?なんでUTCの時刻返ってきてるんだ?あ、そうか。localtimeという概念あった。」

みたいなことを毎回考えている。モヤモヤするので、もうちょっとこの辺の経緯に関する情報を調べてみる。

ドキュメントを読む

とりあえず、Djangoのtimezone.nowに関するドキュメントを読んでみよう。 5.0でのドキュメントである。

Django ユーティリティ (django.utils) | Django ドキュメント | Django

次の記載がある。

USE_TZ が False の場合、システムのローカルタイムゾーンで現在の時刻を表す、関連するタイムゾーンのない naive な日時(タイムゾーンの関連がない日時)になります。

翻訳前

If USE_TZ is True, this will be an aware datetime representing the current time in UTC. Note that now() will always return times in UTC regardless of the value of TIME_ZONE; you can use localtime() to get the time in the current time zone.

USE_TZという概念が関係ありそうだ。

そもそも、 timezone.localtime() のデフォルト

また、ドキュメントを読んでいる時点で

timezone.localtime() はデフォルトでnowを返すことに気づいた。ドキュメントにそう書いてある。

value が省略された場合, デフォルトは now() になります。

Django ユーティリティ (django.utils) | Django ドキュメント | Django

つまり、タイムゾーンを意識した現在時刻は、これで取得すれば良い。

>>> timezone.localtime()
datetime.datetime(2024, 3, 28, 6, 8, 8, 700251, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))

具体的なコードも見てみたが、本当にそのままnowが実行されている。

django/django/utils/timezone.py at 617bcf611f3daa796e4054ba041089ece30a32fc · django/django · GitHub

というわけで、私は timezone.now() ではなく timezone.localtime() をもっと意識して使うと良さそうだ。

USE_TZについて

ここまで調べてみて、最初のめんどくささはある程度低減された。 ただ、せっかくなので、タイトルの問をもう少し深掘りしたい。

timezone.now() のドキュメントで USE_TZ の話が出てくるので、この設定値が関係してそうだ。 その辺をもう少し調べてみる。

USE_TZ

デフォルトで日時がタイムゾーンを意識するかどうかを指定する真偽値です。これが True に設定されている場合、Django は内部的にタイムゾーンを意識した日時を使用します。

Settings | Django documentation | Django

この記載を見て思うこととして、そもそも、この設定は必要なのか?(わざわざ、タイムゾーンを意識しない、という設定をすることがあり得るのか?)と思った。というのも、私は、Djangoを使い始めてから USE_TZ の値を変更したことがない。「デフォルト値から変更しない設定値」という印象で使ってきた。

なので、この設定が導入された経緯を追いかけてみよう。

UZE_TZはいつ導入されたのか

ドキュメントに USE_TZ の記載があるので、バージョンいくつから導入されているのかを確認してみよう

Djangoのドキュメントで閲覧できる最も古いバージョンは、1.8 である。そのドキュメントを見てみると、すでにUSE_TZがある。

Settings | Django documentation | Django

そこで次のことがわかった

  • UZE_TZが指定されていない場合、 Falseで扱われる(つまり、タイムゾーンを意識せずに扱う)
  • django-admin startproject でプロジェクトを作るとUSE_TZ = True が追加される。

というわけで、 1.8 以前に導入されていたことが見えるし、上記の記載からなんとなく以下の状況が推測できた。

  • 最初は、USE_TZがなかった。そして、タイムゾーンを意識せずに扱っていた。
  • データベースに日時を入れたり、結局のところ、タイムゾーンを意識する必要性が高まってきた。というか常にタイムゾーンを意識した方が望ましい状況になってきた。
  • 後方互換性を意識して、新規プロジェクトではデフォルトでタイムゾーンを意識させるように調整が入った。

これは事実かどうかの裏どりはしていない。が、面倒になってきたので、私の調査はここで終えるとする。

timezone.nowがUTCを返す理由もこの辺の兼ね合いなんだろうなあ、と想像した。

USE_TZの今後

少し余談だが、Django 5.0のドキュメントに USE_TZ のデフォルト値はTrueで、古いバージョンではFalseにだった。との情報があった。

Changed in Django 5.0: In older versions, the default value is False.

Settings | Django documentation | Django

これも推測の裏どりをしていないが、やはりそもそもの話として、 USE_TZ という設定値必要か?という話があるんだろうな、と想像した。 デフォルト値がTrueになったことで、 デフォルトの settings.pyから USE_TZ は姿を消すことになるような気がする。

そうすると、この記事に書いたような疑問を解消するのは、少しハードルが上がるようにも思う。 だから、どうという話ではないですが。

終わり

今回は、ふと思ったことを調べてみました。

ではでは〜