[Python] luigiのTask#outputをopenで書き込まない場合

ちょっとニッチな状況でハマったのでメモ

runでファイルコピーするだけのタスクを作った時、ファイルコピーを以下のようにやってしまった。

import shutil
from luigi import Task

class UserTask(Task):

def run(self):
shutil.copy('<source path>', self.output().path)

def output(self):
return LocalTarget('<output path>')

ディレクトリを自動で作ってくれない

shutil.copyでコピーしているので当然と言えば当然だが、outputの親ディレクトリを自動で作成はしてくれない。
self.output().makedirs()を明示的に実行する必要がある。

def run(self):
self.output().makedirs()
shutil.copy('<source path>', self.output().path)

self.output().makedirs()self.output().open('w')で書き込む時は内部的に自動で呼んでくれる。なのでshutil.copyではなく直接書き込めばmakedirs()を明示的に呼ぶ必要はない。

def run(self):
with self.output.oepn('w') as output, open('<source path>', 'r') as input:
output.write(input.read())

ユニットテスト時に MockTarget でパッチできない

パッチできないわけではないけど、意図した動きにならなかった。

# !/usr/bin/env python3
# coding: utf-8

import unittest
from unittest import mock
from luigi import Task
from luigi import build, LocalTarget
from luigi.mock import MockTarget

class UserTask(Task):

def run(self):
self.output().makedirs()
shutil.copy('<source path>', self.output().path)

def output(self):
return LocalTarget('<output path>')


def mocked_output(*args, **kwargs):
m = MockTarget('mock_target')
m.makedirs = lambda: True
return m


class TestUserTask(unittest.TestCase):

@mock.patch('__main__.UserTask.output', side_effect=mocked_output)
def test_method(self, m_mock):
""" test file copy """
result = build([UserTask()], local_scheduler=True)
self.assertTrue(result)


if __name__ == "__main__":
unittest.main()

LocalTargetMockTargetに置き換えるようoutputをモックでパッチしたかった。

しかしoutputファイルの書き出しはself.output().openでやっているわけではないので、MockTargetにしたにも関わらずファイルの実体が書き出されてしまう。

ここもshutil.copyではなくself.output().open('w')でやれば問題なかった。

パッチはするけど LocalTarget で

モックでパッチするけど、MockTargetではなくLocalTargetis_tmpする形にしている。

def mocked_output(*args, **kwargs):
m = LocalTarget(is_tmp=True)
m.makedirs = lambda: True
return m

また、makedirsが呼ばれるとディレクトリを作ったしまうので、適当なメソッド(上記ではラムダ)で上書きしておく

まとめ

  • outputのターゲットは、ちゃんとself.output().open('w')で書かないと意図しない動作することがあるよ!

実行環境

  • Windows 10
  • Python 3.6.3
  • luigi 2.7.2