python 開発関連情報


Develop Environment

PyDev for Eclipseを参照のこと


Source Code Comments

Python では、言語仕様としてファイルヘッダや関数のメソッドのコメントについて 書式を定めていない。コメントの書き方はいくつかの流儀があるが、ここでは 「Google Style Python Docstrings」に基くコメントの書き方を示す。

関数コメントの記述

def myfunction(arg1: str, arg2: int=0):
    '''<関数の概要説明>
    
    <関数の処理内容の詳細説明>
    
    Note:
        <注意事項の説明>
    
    Args:
        <引数名> (<型>): <必須の引数の説明>
        <引数名> (:obj:`<型>`, optional): <省略可能な引数の説明>
        ...
    
    Returns:
        <型>: <戻り値の説明>
    
    Raises:
        <例外クラス>: <例外発生条件の説明>
    
    Examples:
        <例の説明>
    
            <サンプルコード>
    
    '''
    <関数の実装>
注 意

引数の記述で、Google Style の省略可能な引数の形式は、pdoc で API ドキュメントを生成する際にた正しく解釈されない模様である。pdoc で API ドキュメントを生成する際は、「(:obj:``, optional)」と書く代わりに 「(, optional)」と書いてこの問題を回避する。

参考文献

クラスコメントの記述

class MyClass(object):
    '''<クラスの概要説明>

    <クラスの処理内容の詳細説明>

    '''
    def __init__(self, arg1, arg2):
        '''<クラス初期化処理の概要説明>
        
        <クラス初期化処理の詳細説明>
        
        Attributes:
            arg1 (<型>): <必須の引数の説明>
            arg2 (:obj:`<型>`, optional): <省略可能な引数の説明>
        
        '''
        self._myproperty: str = None
        ...
    
    @property
    def myproperty(self):
        '''<型>: <プロパティの説明はsetterでコメントする>'''
        return self._myproperty
    
    @myproperty.setter
    def myproperty(self, value):
        self._myproperty = value
    
    @myproperty.deleter
        del self._myproperty
    
    def classMethod(arg1: str, arg2: int=0):
        '''<メソッドの概要説明>
        
        ※ 関数コメントと同じ内容を記述する
        
        '''
        <メソッドの実装>

pdocによるAPIドキュメントの出力

  1. パッケージのインストール
    $ python -m pip install --user pdoc3
  2. APIドキュメントの生成
    $ python -m pdoc --html -o output-dir --force {package-dir | module-path ...}

    引数の説明

    • -o output-dir - APIドキュメントの出力先ディレクトリを指定する
    • --force 出力結果を上書き更新する
    • package-dir APIドキュメント出力対象のパッケージのディレクトリ
    • module-path APIドキュメント出力対象のモジュールファイルのパス
参考文献

Dependendies Management

作成したモジュール/パッケージが依存するパッケージを管理する手順を説明する。

Requirements File

作成したモジュール/パッケージと同じディレクトリーに、「requirements.txt」 という名前のテキストファイルを配置して、依存するパッケージを記述する。 このファイルを使用して、pip コマンドでパッケージのインストールや アンインストールを行う。

ファイルの名前は特に定められていないが、慣習的に「requirements.txt」が 用いられている。以下に requirements.txt の簡単な例を示す。

#
####### example-requirements.txt #######
#
###### Requirements without Version Specifiers ######
nose
nose-cov
beautifulsoup4
#
###### Requirements with Version Specifiers ######
#   See https://www.python.org/dev/peps/pep-0440/#version-specifiers
docopt == 0.6.1             # Version Matching. Must be version 0.6.1
keyring >= 4.1.1            # Minimum version 4.1.1
coverage != 3.5             # Version Exclusion. Anything except version 3.5
Mopidy-Dirble ~= 1.1        # Compatible release. Same as >= 1.1, == 1.*

[requirements.txt の簡単な例]

Requirements File の詳細については、以下の「参考文献」を参照のこと。

参考文献

依存パッケージのインストール

以下のコマンドを実行して、Requireents File に記述されたパッケージを インストールする。

$ python -m pip install [-t <dir> | --user] -r <requirements-file-path>
  1. オプションの説明
    • -t dir

      指定したディレクトリーにパッケージを配置する。

    • --user

      ユーザーのホームディレクトリの下(~/.local/lib/python3.x/site-packages/) にパッケージをインストールする。

    • -r requirements-file-path

      Requirements File のパスを指定する。

  2. 依存パッケージのインストール先について

    -t オプション、--user オプションを指定しない場合は、システム・ディレクトリー (/usr/lib/python3.x/site-packages) にパッケージがインストールされる。この 場合は、root 権限でコマンドを実行する必要がある。

  3. パッケージのパスの指定について
    • pip install のオプションに --user を指定した場合(ユーザー・ ホームディレクトリーへのインストール)、または -t、--user オプションを 指定しなかった場合(システム・ディレクトリーへのインストール)は、 そのままインストールしたパッケージを使用できる。
    • 実行するモジュールと同じディレクトリーにモジュール/パッケージが配置 されている場合も、そのままパッケージを使用できる。
    • 上記以外のディレクトリーにパッケージが配置されている場合は、以下の対応を する。
      • 環境変数「PYTHONPATH」にパッケージを配置したディレクトリーを指定する
      • パッケージを呼び出すモジュールの中で 「sys.path.append(path)」を実行 して、パッケージを配置したディレクトリーを追加する。これは環境変数 「PYTHONPATH」を設定した場合と同じ効果になる。
      • パッケージを呼び出すモジュールの import 文で相対パスの形式でパッケージを 指定する
補 足

1つのシステム上に多数のモジュール/パッケージを配置して、それぞれの モジュール/パッケージに依存したパッケージのインストール/アンインストール を繰り返す場面では、以下の理由によりモジュールと同じディレクトリーに依存 パッケージをインストールすることを検討した方が良い。

  • それぞれが依存するパッケージのバージョンの衝突の可能性
  • インストール/アンインストールを繰り返すことによる依存パッケージのゴミが 残る可能性
参考文献

依存パッケージのアンインストール

以下のコマンドを実行して、Requireents File に記述されたパッケージを インストールする。

$ python -m pip uninstall [-y] {-r <requirements-file-path>|<package-name> ...}
  • アンインストールの対象は、ユーザー・ホームディレクトリー、またはシステム・ ディレクトリーにインストールされたパッケージである
  • 「-r requirements-file-path」を指定すると、指定された Requirements File に設定されたパッケージをアンインストールする
  • package-name」を指定すると、引数で指定したパッケージをアンインストール する
注 意

アンインストールされるパッケージは指定したパッケージのみで、 アンインストール対象のパッケージをインストールした際にそれに依存して インストールされたパッケージはアンインストールされないことに注意すること。

依存関係の表示

pip-licenses を使用して、依存関係にあるパッケージとそのライセンスを表示する 手順を説明する。pip-licenses は、pip でインストールされたモジュールで、python のパスが通っているもの(ただし、pip、setuptools などのシステム・パッケージや pip-licenses を除く)について、ライセンスを表示する。従って、あるパッケージが 依存するモジュールのライセンスのみを表示するためには、python の環境に対象 パッケージ以外のパッケージが pip でインストールされていないことが必要である。

  1. 準備
    • Building from sourceの手順に従い、python をソースコードからビルド して、現在運用している python とは別のディレクトリに python を配置する。
    • ユーザ領域にインストールされている python モジュールを削除する。
  2. 依存パッケージ及び pip-licenses のインストール

    作業ディレクトリに移動し、以下のコマンドを実行して、対象パッケージが 使用するモジュールと pip-licenses パッケージをインストールする。

    $ pythom -m pip install -t site-packages -r <package-dir>/requirements.txt \
              pip-licenses
  3. H依存関係にあるパッケージとそのライセンスをHTML形式で出力
    $ PYTHONPATH=./site-packages python -m piplicenses --format=html \
      --output-file <html-file-path> 

    オプションの説明

    • --format=format-name : 出力形式を指定する
    • --output-file file-path : 出力ファイルのパスを指定する
参考文献

+ GitHub - raimon49/pip-licenses: Dump the license list of packages installed with pip.


Unit Test

python 標準の unittest を使用してテストクラスを実装、実行する手順を説明する。

テストクラスの作成

テストクラスの実装例(TestStringMethods.py)を以下に示す。これは、 https://docs.python.org/ja/3/library/unittest.html#basic-exampleに 記載されているコードである。

import unittest

class TestStringMethods(unittest.TestCase):

    def setUp(self):
        logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
        logging.debug('Set up test.')

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)
    
    def tearDown(self):
        logging.debug('Tear down test.')

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

[TestStringMethods.py]

説明

  • unittest.TestCase クラスを継承したクラスを作成する。
  • テストメソッドは、メソッド名が「test_」で始まるようにする。
  • (オプション)テストメソッドに共通する初期化処理、終了処理があれば、それぞれ setUpメソッド、tearDownメソッドに記述する。
  • テストの判定は、TestCase クラスの assertXxx メソッドを使用して判定する。
  • テストクラスを記述した python ファイルの最後に下記のコードを記述して、 python コマンドからテストクラスのスクリプトを呼び出した場合に限って テストメソッドを実行するようにする。
    if __name__ == '__main__':
        unittest.main()
  • テスト失敗時に指定されたメッセージを表示する場合

    assertXxx メソッドに msg キーワード引数を追加してメッセージを指定する。

        def test_upper(self):
            self.assertEqual('foo'.upper(), 'FOO', msg='All characters must be uppcer case.')

    ただし、assertRaises()、assertRaisesRegex()、assertWarns()、 assertWarnsRegex() の各メソッドについては、これらをコンテキストマネージャと して使用した場合(with キーワードを使用した場合)のみ msg キーワード引数が 有効となることに注意すること。

        def test_split(self):
            s = 'hello world'
            # check that s.split fails when the separator is not a string
            with self.assertRaises(TypeError, msg='TypeError exception must be raised.'):
                s.split(2)
参考文献

unittest の実行

テストクラスを実装した python ファイルを指定して、python コマンドを実行する。 実行結果は、以下のように標準出力に表示される。

  • テストがすべて合格だった場合
    ----------------------------------------------------------------------
    Ran 1 test in 0.000s
  • 不合格のテストがあった場合
    F
    ======================================================================
    FAIL: test_find_repeat (__main__.TestMyModule)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/usr/home/foo/git/python-prog/src/test/python/TestMyModule.py", line 31, in test_find_repeat
        self.assertEqual(mymodule.find_repeat('abcijkxyz', 'ijk', 4, 7), [3, 1], \
    AssertionError: Lists differ: [-1, 0] != [3, 1]
    
    First differing element 0:
    -1
    3
    
    - [-1, 0]
    + [3, 1] : 'ijk' must be found at index:3  of 'ijk' and 1 repeat.
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.004s
    
    FAILED (failures=1)

テスト結果のHTML形式でのレポート出力

  1. 準備

    テストモジュールを配置したディレクトリーに移動して、以下のコマンドを実行し、 html-testRunner パッケージをインストールする。

    $ python -m pip install -t . html-testRunner
    備 考

    -t target-dir オプションについて

    • このオプション指定すると、target-dir ディレクトリーの下に引数で 指定されたパッケージがインストールされる。
    • target-dir/bin ディレクトリの下にパッケージと一緒にインストール されたコマンドがインストールされる。
  2. HtmlTestRunner の使用

    テストクラスの実装は変えずに、テストクラスのスクリプトに以下の処理を追加 する。テストクラスのスクリプトを実行すると、テストクラスのスクリプトを配置 したディレクトリー配下の report-html/python-progs.html にレポートが出力 される。

    1. HtmlTestRunner パッケージをインポートする。
    2. HtmlTestRunner のインスタンスを作成して、main メソッドに渡す。

      具体的には、「if __name__ == '__main__':」の部分で以下の処理を追加する。

      • HtmlTestRunner.HTMLTestRunner メソッドに output キーワード変数で HTML形式のレポートの出力先ディレクトリを指定して、HtmlTestRunner の インスタンスを作成する。
      • 作成したインスタンスを testRunner キーワード引数に指定して unittest.main メソッドを実行する。

      テストクラスのソースコードの実装例は以下のとおり。

      import unittest
      import HtmlTestRunner
      import os
      
      class TestStringMethods(unittest.TestCase):
      
          def test_upper(self):
              self.assertEqual('foo'.upper(), 'FOO')
          
          # テストメソッドは以下省略
      
      if __name__ == '__main__':
          html_runner = HtmlTestRunner.HTMLTestRunner( \
                   output=os.path.dirname(__file__) + '/report-html', add_timestamp=False)
          unittest.main(testRunner=html_runner)

      [TestStringMethods.py]

  3. 複数のテストクラスを一度に実行する

    上記の方法では、テストクラス毎にテストの実行を行い、HTML形式のレポートは テストクラス毎に出力される。一度に複数のテストクラスのテストを実行し、 テスト結果のHTML形式のレポートを1つにまとめるための手順を以下に示す。

    以下の python コードは、mytest1.py に MyTest1 テストクラス、mytest2.py に MyTest2 テストクラスが定義されている場合に、これらのスクリプトを一度に実行 して、その結果をこのスクリプト(test_all.py)と同じディレクトリの report-html/python-progs.html ファイルに出力する処理の例である。

    from unittest import TestLoader, TestSuite
    from HtmlTestRunner import HTMLTestRunner
    import io
    import os
    
    import yaml
    
    def load_config():
        config_path = os.path.splitext(__file__)[0] + '.yml'
        with io.open(config_path, 'r') as file:
            config = yaml.load(file)
            return config
    
    def run_test_all():
        # Loding config file
        config = load_config()
        # Getting test script patterns form config
        patterns = config["module_patterns"]
    
        # Getting test suites from script patterns.
        suite = TestSuite()
        for pattern in patterns:
            suite.addTest(TestLoader().discover(os.path.dirname(__file__), pattern))
    
        runner = HTMLTestRunner( \
                output=os.path.dirname(__file__) + '/../target/site/test-report', \
                report_name='python-progs', add_timestamp=False, combine_reports=True)
        
        runner.run(suite)

    [test_all.py]

    module_patterns:
      - '*Test.py'
      - 'Test*.py'

    [test_all.yml]

    説明

    • test_aall.yml から TestCase スクリプトのファイル名のパターンを読み込む
    • 読み込んだパターンごとに TestLoaderのdiscover()を呼び出してTestSuiteを 取得する
    • HTML形式のレポートの出力先などを指定して、HTMLTestRunner クラスの インスタンスを作成する。
    • HTMLTestRunner の run メソッドに TestCase を指定してテストを実行する。
注 意
  • AttributeError: 'HtmlTestResult' object has no attribute '_count_relevant_tb_levels'.

    テストに失敗すると、HtmlTestRenner を実行ときに下記のエラーが発生する。

    AttributeError: 'HtmlTestResult' object has no attribute '_count_relevant_tb_levels'.

    このエラーは、Pythonのバージョンが上がったことにより、HtmlTestResult クラスの継承元クラス TextTestResult の _count_relevant_tb_levels 属性が 廃止されたことが原因である。このエラーを回避するためには、HtmlTestRunner パッケージの result.py ファイルを以下のように修正する。

    • 修正前
              if exctype is test.failureException:
                  # Skip assert*() traceback levels
                  length = self._count_relevant_tb_levels(tb)
                  msg_lines = traceback.format_exception(exctype, value, tb, length)
    • 修正後
              if exctype is test.failureException:
                  # Skip assert*() traceback levels
                  msg_lines = traceback.format_exception(exctype, value, tb)

    sed コマンドで修正する例は以下のとおり。

    $ sed -e 's/^[ \\t]*length = self._count_relevant_tb_levels(tb)$//g' \
          -e 's/msg_lines = traceback.format_exception(exctype, value, tb, length)/msg_lines = traceback.format_exception(exctype, value, tb)/g' \
          -i <python-lib-dir>/site-packages/HtmlTestRunner/result.py
参考文献

unittest.mock

  1. mockの用途
    • メソッドにパッチを当てる
    • オブジェクトに対するメソッド呼び出しを記録する
  2. Mock、MagicMock クラスの機能概要
    • MagicMock クラスについて

      MagicMock クラスは Mock クラスと互換のより強力な機能を持つので、MagicMock クラスの利用が推奨されている。

    • assert_called_with 関数の利用

      unittest.mock パッケージの Mockクラスは、このインスタンスの属性 (プロパティやメソッド)にアクセスすると、その属性、関数が自動的に定義 される。自動的に作成されたメソッドに対して、assert_called_with 関数を呼び出すと、メソッドの呼び出しをアサートすることができる。

    • Mock オブジェクト(関数・メソッドの場合)の return_value 属性の利用

      Mock オブジェクトの return_value 属性に値を設定すると、Mock オブジェクト にアクセスすると戻り値が return_value 属性に設定した値となる。

    • Mock オブジェクト(関数・メソッドの場合)の side_effect 属性の利用
      • side_effect 属性に関数を設定すると、Mock オブジェクトにアクセスした場合 にその関数が実行される。モックオブジェクトに引数を渡すとそれが side_effect 属性で設定した関数に渡される。
      • side_effect 属性に例外クラス、または例外クラスのインスタンスを設定すると Mock オブジェクトにアクセスした場合にその例外が発生する。
      • side_effect 属性に iterable (listなど)を設定すると、Mockオブジェクトを 呼び出す順番で iterable に設定した値がその順に返される。
  3. テスト対象のオブジェクトへの Mock オブジェクトの適用方法
    • モジュールの属性を Mock オブジェクトに置換する方法

      モジュールの属性を Mock オブジェクトに置き換える場合は、 unittest.mock.patch を使用する。patch 関数をコンテキスト・マネージャーと して使用すると、as で置換されたオブジェクトを参照でき、 コンテキスト・マネージャーのスコープを抜けると置換対象のオブジェクトの 属性は元に戻る。

      • patch 関数のパラメータ

        unittest.mock.path(モジュールの属性名 ,[置換するオブジェクト], [キーワード=[, ...]])

        • モジュールの属性名は、Mock オブジェクトに置換する対象のモジュールの 属性名をパッケージ名、モジュール名を含む形式で指定する。

          例:'モジュール名.勘数名'、パッケージ名.モジュール名.勘数名

        • 置換するオブジェクトを省略すると 置換対象のオブジェクト属性が Mock オブジェクトに置換される。
        • キーワード= は、置換されたオブジェクトに渡されるパラメータで ある。
    • オブジェクトの属性を Mock オブジェクトに置換する方法

      unittest.mock.patch.object 関数に置換対象のオブジェクトと属性名 (例:クラスのメソッド)を指定して、それを Mock オブジェクト に置き換える。patch 関数も patch.object と同様にコンテキスト・マネージャー として利用できる。

      • patch.object 関数のパラメータ

        unittest.mock.path.object(オブジェクト, オブジェクトの属性名, [置換するオブジェクト], [キーワード=][, ...]])

        • 置換するオブジェクトは、Mockに置換する属性のオブジェクト(クラス、 インスタンスなど)を指定する。
        • オブジェクトの属性名は、Mockに値化する対象の属性(例:メソッドなど) の属性名を指定する。
        • 置換するオブジェクトキーワード=は、patch 関数と同様
    参考文献
  4. Mock オブジェクトの使用例
    • 属性や関数の呼び出しをテストする

      モジュールの関数やクラスのメソッドの動作をテストするのではなく、それらが 正しい引数で呼び出され方をテストしたい場合に利用する。

      import unittest
      from unittest.mock import patch
      
      class MyClass():
          '''テスト対象のクラス
          '''
          def __init__(self):
              self.attr1 = None
      
          def doSomething(self, no: int):
              print(f'No.{no} Good morning.')
      
          def doAnother(self, no: int):
              print(f"No.{no} Good by.")
      
      class TestMyClass(unittest.TestCase):
          def testAccess(self):
              '''MyClassの属性、メソッドの呼び出し確認をする
              MyClass の属性やメソッドが正しい引数で呼び出されたことをテストする。
              '''
              with patch('MockTest.MyClass') as mock:
                  # MyClass のインスタンスを Mock オブジェクトから取得する
                  instance = mock.return_value
                  # nyClass(実はMockオブジェクト)のメソッド、属性にアクセスする
                  instance.doSomething(1)
                  instance.doAnother(2)
                  instance.attr1 = 3
      
                  # MyClass の doSomething メソッドが引数「1」を指定して呼び出された
                  # ことを確認する
                  instance.doSomething.assert_called_with(1)
                  # MyClass の doAnother メソッドが引数「2」を指定して呼び出された
                  # ことを確認する
                  instance.doAnother.assert_called_with(2)
    • 関数・メソッドの戻り値を設定してテストする

      メソッドAをテストしたいが、メソッドAが内部で呼び出しているメソッドBの 実行が難しい環境なので、メソッドBをを実行せずにメソッドBが 「Hello world.」を返すことにしてテストを実行する。

      import requests
      import unittest
      from unittest.mock import patch
      
      class MyClass():
          '''テスト対象のクラス
          '''
          def methodA(self, url):
              return self.methodB(url)
          
          def methodB(self, url):
              response = requests.get(url)
              result = response.text
              return result
      
      class TestMyClass(unittest.TestCase):
          def testMethodA(self):
              '''MyClass の methodA のテストを実行する
              methodBの内部で request.get により呼び出している Web API を実行できない
              ので、methodB が 'Hello world.' が返すことにしてテストを実行する。
              '''
              instance = MyClass()
              # オブジェクト(MyClass)の methodB を Mock オブジェクトに置換し、
              # 置換されたMock オブジェクトの return_value に 'Hello world.' を設定
              with patch.object(MyClass, 'methodB', return_value='Hello world.'):
                  result = instance.methodA('http://www.foo.bar.xyz/func1/')
                  self.assertEqual(result, 'Hello world.')
    • 関数・メソッドを呼び出す順に異なる値を返すように設定してテストする

      上記と同様な場合だが、メソッドBが毎回同じ値を返すのではなく、呼び出す 順番に異なる値を返すようにしてテストする。

      import unittest
      from unittest.mock import patch
      
      class MyClass():
          '''テスト対象のクラス
          '''
          def methodA(self, url):
              return self.methodB(url)
          
          def methodB(self, url):
              response = requests.get(url)
              result = response.text
              return result
      
      class TestMyClass(unittest.TestCase):
          def testMethodAMultiple(self):
              '''呼び出される度に戻り値が異なるメソッドをモックしてテストする
              methodBの内部で request.get により呼び出している Web API を実行できない
              ので、methodB が 'Hello world.'、'Good by.'を順に返すことにしてテストを
              実行する。
              '''
              myClass = MyClass()
              # オブジェクト(MyClass)の methodB を Mock オブジェクトに置換し、
              # 置換されたMock オブジェクトの return_value に 'Hello world.' を設定
              with patch.object(MyClass, 'methodB',
                      side_effect=['Hello world.', 'Good by.']):
                  # methodA から medhodB への1回目の呼び出し
                  result = myClass.methodA('http://www.foo.bar.xyz/func1/')
                  self.assertEqual(result, 'Hello world.')
                  
                  # methodA から medhodB への2回目の呼び出し
                  result = myClass.methodA('http://www.foo.bar.xyz/func1/')
                  self.assertEqual(result, 'Good by.')
    • メソッドにアクセスしたら例外が発生させてでテストをする

      例外を発生させるのが難しい状況で、例外発生パターンをテストしたい場合に 利用する。

      import requests
      import unittest
      from unittest.mock import patch
      
      class MyClass():
          '''テスト対象のクラス
          '''
          def methodB(self, url):
              response = requests.get(url)
              result = response.text
              return result
      
      class TestMyClass(unittest.TestCase):
          def testMethodBException(self):
              '''requests.get 関数の例外発生テストをする。
              requests.get を実行したら ConnectionError が発生した場合のテストを
              実行する。
              '''
              myClass = MyClass()
              with patch('requests.get', side_effect=requests.ConnectionError):
                  with self.assertRaises(requests.ConnectionError):
                      myClass.methodB('http://www.foo.bar.xyz/func1/')
    • パッケージの関数を置換してテストする

      パッケージの関数の実行が難しい場合、例えば、Web APIを呼び出す機能をテスト したいがテスト環境ではWeb APIにアクセスできない場合に Web API の呼び出し 処理を別の関数に代替させてテストを実行する。

      import requests
      import unittest
      from unittest.mock import patch
      
      class MyClass():
          '''テスト対象のクラス
          '''
          def methodB(url):
              response = requests.get(url)
              result = response.text
              return result
      
      class MyResponse():
          '''requests.Responseオブジェクトの代わりに使用するオブジェクト
          '''    
          def __init__(self, status_code, text):
              self.status_code = status_code
              self.text = text
      
      def mock_get(url) :
          '''request.get メソッドの代わりに使用するメソッド
          urlの最後の「/」とその前の「/」との間の文字列が「func1」の場合は、
          「200: Success.」を返し、それ以外の場合は「404: Error.」を返す。
          '''
          items = url.split('/')
          action = items[len(items) - 2]
          if action == 'func1':
              return MyResponse(200, 'Success.')
          else:
              return MyResponse(404, 'Error.')
      
      class TestMyClass(unittest.TestCase):
          def testMethodReplace(self):
              '''requests.getメソッドをテスト用のロジックに置き換えてテストする
              request.getメソッドで呼び出すWeb APIが実行できないので、テスト用の
              ロジックに置き換えてテストする。
              '''
              instance = MyClass()
              # requestsモジュールの get 関数を mock_get 関数に置き換える
              with patch('requests.get', side_effect=mock_get):
                  response = instance.methodB('http://www.foo.bar.xyz/func1/')
                  self.assertTrue(response, 'Success.')
                  response = instance.methodB('http://www.foo.bar.xyz/func2/')
                  self.assertTrue(response, 'Error.')
参考文献

Coverage Report

  1. 準備

    テストスクリプトを配置したディレクトリーに移動して以下のコマンドを実行し、 coverage パッケージをインストールする。

    $ python -m pip install -t . coverage
  2. Coverage report の出力
    1. カバレッジデータの収集

      以下のコマンドを実行して、テストを実行しカバレッジデータを収集する。 「-t target-dir」オプションを指定して coverage パッケージを インストールした場合は、「target-dir/bin/coverage」を実行すること。

      $ coverage run --branch <test-script-path>

      上記のコマンドを実行すると、収集された条件網羅のカバレッジデータが カレントディレクトリーの「.coverage」ファイルに出力される。

    2. カバレッジレポートの出力

      以下のコマンドを実行して、HTML形式のカバレッジレポートを出力する。

      $ coverage html

      上記のコマンドを実行すると、カレントディレクトリー配下の「htmlcov」 ディレクトリーの下に、HTML形式のカバレッジレポートが出力される。

参考文献

Proxy Settings

プロキシサーバーを通してインターネットにアクセスするときの設定について説 明する。

Python コマンドを使用する場合

  • 環境変数で設定する場合

    Python プログラムがインターネットにアクセス可能にするためには、下記の環境 変数にプロキシサーバーの設定をする。

    • http_proxy

      httpプロトコルで通信する場合は、この環境変数を設定する。設定する値は、 認証のユーザー名、パスワード、プロキシサーバー名、ポート番号を以下の 形式で指定する。

      http://[<ユーザ名>:<パスワード>@]<プロキシサーバー名>[:<ポート番号>]
    • https_proxy

      httpsプロトコルで通信する場合は、この環境変数を設定する。設定する値は、 http_proxy 環境変数の同じである。

    • no_proxy

      プロキシサーバーを経由せず、直接アクセスするサーバー、IPアドレスを指定 する場合は、この環境変数を設定する。設定する値は、除外するサーバー名、 IPアドレスの suffix (後方一致の文字列)で、複数の場合はカンマで区切る。

      "<サーバー名のsuffix>[:<ポート番号>],<IPアドレスのsuffix>,..."
  • 設定ファイルで設定する場合

    設定ファイルで読み取った値を以下のようにプログラムで環境変数に設定する。

    import os
    os.environ['http_proxy'] = <設定ファイルから読み取った値>
    os.environ['https_proxy'] = ...
    os.environ['no_proxy'] = ...

pip を利用する場合

pip コマンド、または、「python -m pip」コマンドを実行する際に、プロキシ サーバー経由でインターネットにアクセスするときの設定は、以下のとおりである。

  • コマンドのパラメータで指定する場合

    pip コマンド、または「python -m pip」コマンドに以下のパラメータを指定する。

    --proxy=http://[<ユーザ名>:<パスワード>@]<プロキシサーバー名>[:<ポート番号>]
  • 設定ファイルで設定する場合

    python の実行プログラムと同じディレクトリに pip.ini ファイルを配置して、 以下のように設定する。

    [global]
    proxy = http://[<ユーザ名>:<パスワード>@]<プロキシサーバー名>[:<ポート番号>]

Virtual Environments

Pythonの仮想環境は、ベースのPython(システムにインストールされているPythonの 環境)と独立したPythonの環境を提供する物である。仮想環境を構築すると、 pythonの十個環境や site-packages がベースのPythonととは別に容易され、それを 利用することが可能になる。

  1. 仮想環境の作成

    以下のコマンドを実行して、仮想環境を作成する。

    $ python3 -m venv <仮想環境作成ディレクトリ>

    仮想環境のディレクトリは、「.venv」が推奨される。

  2. 仮想環境の有効化

    以下のコマンドを実行して、仮想環境を有効化する。

    $ source <仮想環境作成ディレクトリ>/bin/activate

    仮想環境を有効化すると、シェルプロンプトに括弧付きの仮想環境作成 ディレクトリの名前が表示される。以降このシェルプロンプトでpythonコマンドを 実行すると、仮想環境作成ディレクトリの下に作成されたPythonの環境が適用 される。

  3. 仮想環境の無効化

    仮想環境が有効な状態で以下のコマンドを実行すると仮想環境が無効化され、 シェルプロンプトから括弧付きの仮想環境のディレクトリ名が表示されなくなる。

    (<仮想環境ディレクトリ名>) $ diactivate
参考文献

Building from source

  1. Pythonビルド用の依存パッケージのインストール
    # dnf install dnf-plugins-core
    # dnf builddep python3
  2. ソースファイルのダウンロード

    https://www.python.org/downloads/source/より Python 3.x.x の Gzipped source tarball (Python-3.x.x.tgz) をダウンロードする。

  3. ソースファイルからのビルド

    「tar xvfz Python-3.x.x.tgz」を実行して、ダウンロードしたファイルを解凍し、 解凍先のディレクトリ(Python-x.x.x)に移動して以下のコマンドを実行する。

    $ ./configure --enable-optimizations
    $ make
    $ make altinstall DESTDIR=<work-dir>

    上記のコマンドを実行した結果、work-dir/usr/local ディレクトリの下に、 Pythonnの bin、include、lib、share ディレクトリーが作成される。

    注 意
    • make コマンド実行時にコンパイルエラーが発生する場合は、configure コマンドの「--enable-optimizations」オプションを外して実行する。
  4. 実行ファイルのシンボリック・リンク作成

    work-dir/usr/local/bin ディレクトリ配下の実行ファイル「xxx3.xx」に対する シンボリック・リンクを作成し、名前を「xxx3」にする。作成するシンボリック・ リンクは以下のとおりである。

    • 2to3-3 -> 2to3-3.xx、2to3 -> 2to3-3.xx
    • idle3 -> idle3.xx、idle -> idle3.xx
    • pip3 -> pip3.xx、pip -> pip3.xx
    • pydoc3 -> pydoc3.xx、pydoc -> pydoc3.xx
    • python3 -> python3.xx、python -> python3.xx
    • python3-config -> python3.xx-config、python-config -> python3.xx-config
  5. ファイルの配置

    work-dir/usr/local ディレクトリを名前を変えて、適切な場所に配置する。 例えば、/opt/python3 ディレクトリの下に Python の各ディレクトリを配置する 場合は、以下のコマンドを実行する。

    # cp -Rvi <work-dir>/usr/local /opt/python3
参考文献

Windows embeddable package

Windows版のPythonには、zipを解凍したらすぐに使用可能となる embeddable package がある。ただし、python コマンドしかないため、pip によるパッケージの インストールを可能とするためには、そのための手順が必要である。

embeddable package のダウンロードとセットアップ

https://www.python.org/downloads/windows/ より 「Windows embeddable package (64-bit)」のリンクをクリックして zip ファイルを ダウンロードし、配置先のディレクトリに移動して zip ファイルを解凍する。

pip のインストール

  1. pythonバージョン._pth ファイルの編集

    embeddable package を解凍したディレクトリに存在する pythonバージョン._pth を以下のように編集する。

    • 「#import site」の先頭のコメントを外す
    • 1行を追加して「./Lib/site-packages」を追加する
  2. current.pth ファイルの作成

    embeddable package を解凍したディレクトリに「current.pth」ファイル (ファイル名は任意だが、拡張子は .pth とすること)を作成し、以下の内容を 設定する。

    sys.path.append('')
  3. pip インストーラーのダウンロードと実行

    https://bootstrap.pypa.io/get-pip.py をダウンロードして作業ディレクトリに 配置し、作業ディレクトリに移動して以下のコマンドを実行する。

    $ <embeddableパッケージ配置ディレクトリ>\python.exe get-pip.py

django tutorial - Web Application Framework

インストール

$ python -m pip install [--user] Django
参考文献

クイックインストールガイド

プロジェクトの作成

まだプロジェクトを作成していない場合は、プロジェクトを作成する。 Webアプリケーションは、プロジェクトに配置される。

  1. プロジェクトの作成

    プロジェクトを作成するディレクトリに移動して、以下のコマンドを実行する。

    $ django-admin startproject <プロジェクト名>

    【注意】

    • プロジェクト名 は「-」を含む記号を含めないこと

    プロジェクト作成コマンドを実行すると、プロジェクト名で指定した名前の ディレクトリが作成され、更にその下にプロジェクト設定用の同名のディレクトリ (Pythonパッケージ)が作成される。

    <プロジェクト名> -- プロジェクト・コンテナ
    |-- manage.py
    `-- <プロジェクト名> -- Pythoんパッケージ
        |-- __init__.py
        |-- asgi.py
        |-- settings.py
        |-- urls.py

    [プロジェクトディレクトリの構成]

    以降は、上位のプロジェクト名のディレクトリ(manage.py が配置された ディレクトリ)を「プロジェクト・コンテナ」と表現する。

    参考文献

    はじめての Django アプリ作成、その 1

  2. プロジェクトの設定

    プロジェクト・コンテナの プロジェクト名/settings.py に対して以下の設定をする。

    • データベースの設定

      規定では sqlite3 を使用するように設定されている。データベースに sqlite3 を 使用する場合は、変更せずに使用可能である。

      他のRDBを使用する場合は、以下の部分を設定し、データバインド(PostgreSQL の場合は、psycopg2 パッケージ)をインストールする。

      DATABASES = {
          'default': {
              'ENGINE': 'django.db.backends.postgresql',
              'NAME': 'mydatabase',
              'USER': 'mydatabaseuser',
              'PASSWORD': 'mypassword',
              'HOST': '127.0.0.1',
              'PORT': '5432',
              'ATOMIC_REQUESTS': True|False
          }
      }

      [PostgreSQLの設定例]

      • ENGINE - 以下の何れかを指定する。
        • 'django.db.backends.postgresql'
        • 'django.db.backends.mysql'
        • 'django.db.backends.sqlite3'
        • 'django.db.backends.oracle'
      • NAME - データベースの名前
      • USER - データベース作成の権限を持つデータベースユーザー名。
      • PASSWORD - データベースユーザーのパスワード
      • HOST - データベースの名前解決可能なホスト名、またはIPアドレス
      • PORT - データベースサーバーに接続するときのポート番号
      • ATOMIC_REQUESTS - True: ビュー関数の呼び出し前にトランザクションを 開始し、ビュー関数でレスポンスが正常に生成されたらトランザクションを コミットする。False: オートコミットモードが有効になる。

      以上の設定を終えたら、プロジェクト・コンテナに移動して、以下のコマンドを 実行してアプリケーションの実行に必要なデータベースのテーブルを作成する。

      $ python manage.py migrate
    • タイムゾーンの設定

      「TIME_ZONE =」にタイムゾーンの設定をする。

      TIME_ZONE = 'Asia/Tokyo'

      [タイムゾーンの設定例]

  3. 開発サーバーの起動

    プロジェクト・コンテナで以下のコマンドを実行して、Django の アプリケーションを実行する。

    $ python manage.py runserver [{<ポート番号> | <IPアドレス>[:<ポート番号>]}]
    • IPアドレスポート番号の指定を省略した場合は、Django アプリケーション は、「http://127.0.0.0:8000/」を指定してアクセスする。
    • サーバーの待ち受けアドレスを限定しない場合は、runserverのパラメータに 「0.0.0.0:ポート番号」を指定する。
参考文献

Database の設定 - はじめての Django アプリ作成、その2

Django Admin (admin サイト)の設定

Django Admin は、コンテンツ(アプリケーションのモデル)の追加、変更、削除 などを管理するための Webアプリケーションである。利用開始の前のに、以下の 設定をする。

  1. 管理ユーザーの作成

    プロジェクト・コンテナに移動して、以下のコマンドを実行する。

    $ python manage.py createsuperuser

    以下の入力を要求されるので、それぞれを設定する。

    • Username - 管理者のログイン名を指定する
    • Email address - 管理者のメールアドレスを指定する
    • Password - 管理者のログインパスワードを指定する
  2. adminサイトへのアクセス

    http://127.0.0.1:8000/admin/ にアクセスして、管理者のログイン名、 パスワードでログインし、adminサイトを表示する。

参考文献

管理ユーザーを作成する - はじめての Django アプリ作成、その2

Webアプリケーションの構築

  1. アプリケーションの作成

    プロジェクト・コンテナに移動して、以下のコマンドを実行する。

    $ python manage.py startapp <アプリケーション名>

    【注意】

    • アプリケーション名 は「-」を含む記号を含めないこと

    アプリケーション作成コマンドを実行すると、アプリケーション名で指定した 名前のディレクトリが作成され、そこにアプリケーションのディレクトリ、 ファイルが作成される。

    参考文献

    Polls アプリケーションをつくる - はじめての Django アプリ作成、その 1

  2. アプリケーションの構成クラスの参照設定

    プロジェクト名/settings.py のファイルを開き、以下の設定を追加する。

    INSTALLED_APPS = [
        '<アプリケーション名>.apps.<構成クラス名>',
        ...
    ]
    • 構成クラス名は、アプリケーション名/apps.py に設定されている AppConfig の継承クラスの名前を指定する。
    参考文献
  3. モデルの作成
    1. モデルの定義

      モデルはデータベースのテーブル定義を格納するクラスである。 アプリケーション名/models.py ファイルにモデルの定義を記述する。

      モデルには、以下の内容を定義する。

      • テーブルのフィールド(項目名、型)
      • __str__ 関数。1つのレコードを表現する文字列
      • モデルに対する操作

      以下に示すものは、Django公式ドキュメントの 「はじめてのDjangoアプリ作成、その2」に記載されているモデルの定義である。

      import datetime
      from django.db import models
      from django.utils import timezone
      
      # Create your models here.
      
      class Question(models.Model):
          question_text = models.CharField(max_length=200)
          pub_date = models.DateTimeField('date published')
      
          def __str__(self):
              return self.question_text
      
          def was_published_recently(self):
              return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
      
      class Choice(models.Model):
          question = models.ForeignKey(Question, on_delete=models.CASCADE)
          choice_text = models.CharField(max_length=200)
          votes = models.IntegerField(default=0)
      
          def __str__(self):
              return self.choice_text

      [モデルの定義例]

      参考文献

      モデルの作成 - はじめてのDjangoアプリ作成、その2

      モデルの自動生成API

      以下の説明で「instance」はモデルのインスタンスを格納した変数を表わす。 また、「Model」はモデルのクラス名を表わす。

      • Django の概要で説明されているもの
        • Model.objects.all()

          データベースからモデルの全レコードを検索し、Model のインスタンス の配列を返す。

        • Model.objects.get(field-name=value)

          Model のフィールド field-name の値が value と一致する レコードを検索し、Modelのインスタンスを返す。該当するレコードが ない場合、または該当するレコードが複数存在する場合は例外が発生する。

        • Model.objects.get(field-name__startswith=value)
          Model.objects.get(field-name__contains=value)

          Model のフィールド名 field-name の値が valueで始まる (__startswidth)/valueを含む(__contains)レコードを検索し、 Model のインスタンスを返す。該当するレコードがない場合、 または該当するレコードが複数存在する場合は例外が発生する。

        • Model.objects.filter(field-name__startswith=value)

          Modelのフィールド名 field-name の値が valueで始まるレコードを 検索し、Model のインスタンスの配列を返す。

        • instance.model_set.all()

          modelModel の先頭を小文字にしたもの。Modelの外部キーが instanceを参照している Model のインスタンスをすべて取得して、 その配列を返す。

          【補足】

          • Ariticle モデルの reporter フィールドが Reporter を外部参照して いる場合は、r が Reporter モデルのインスタンスである場合、 「r.article_set.all()」は r インスタンスを外部参照 している Article のインスタンスすべてを取得する。
            .-----------.
            |Article    |      .--------.
            |* reporter-+----->|Reporter|
            `-----------'      `--------'
          • model_set は QuerySet なので、all() 以外にも filter() を 指定することができる。
        • instance.save()

          オブジェクトを新規作成または変更する。

      • viees.py で使用されているもの
        • Model.objects.filter(field-name-1__lte=tvalue).order_by('field-name-2')

          Model の フィールド名 field-name-1 の値が value 以下の レコードを取得し、field-name-2の昇順にソートしたものを Modelの インスタンスの配列で返す。order_by の field-name-2の前に「-」を 付けると降順のソートになる。

        • instance.model_set.get(field-name=value)

          instanceModelの外部キーで参照している Model のインスタンス から フィールド名 field-name の値が valueと一致するものを取得 して Model のインスタンスの配列で返す。

    2. テーブルの作成

      プロジェクト・コンテナのディレクトリで以下のコマンドを実行して、モデルの 定義からデータベースを作成する。

      $ python manage.py makemigrations polls

      なお、このコマンドの実行により以降のモデルの変更はデータベースに反映 されるとともにマイグレーションの記録として 「アプリケーション名/migrations」ディレクトリの下にテキストファイルで 記録されるようになる。

  4. admin サイトへのモデルの登録

    アプリケーション名/admin.py ファイルに対して以下の設定をする。

    from django.contrib import admin
    from .models import <モデルクラス名>
    
    # Register your models here.
    admin.site.register(<モデルクラス名>)
    参考文献

    Poll アプリを admin 上で編集できるようにする

  5. URLディスパッチャ

    URLディスパッチャは、リクエストURLをビューの関数に紐付ける。 URLディスパッチャは、以下の2つのファイルで設定する。

    • アプリケーション名/urls.py

      URLとビューの関数の関連付けをこのファイルに設定する。urlpatterns に 以下のようにpath関数の戻り値を設定する。なお、下記のコードの app_name 変数はアプリケーションの名前空間を指定するもので、テンプレートでURLを 指定するときに使用する。

      from django.urls import include, path
      from . import views
      
      app_name = 'polls'
      urlpatterns = [
          path('<int:question_id>/results/', views.results, name='results'),
          ...
      }

      URLのディスパッチは以下のように行われる。

      1. URIからサーバー名の後の「アプリケーション名/」までを取り除く。 アプリケーション名は、apps.py の「name=」に設定されたものを使用する。
      2. 上記の文字列にpath関数の第1引数が最長マッチしたものが適用され、 path関数の第2引数で指定された関数を実行する。
      3. URIがマッチした文字列の前に中に「<パラメータ名>」が含まれている 場合は、マッチした文字列の該当部分を「パラメータ名」のパラメータ名で 実行される関数に渡す。「パラメータ名」の前に「int:」が指定された場合は 整数型に変換してから関数に渡される。

      URLディスパッチの詳細は、django topicsURL Dispatcher を参照のこと。

      【例】

      1. ブラウザから「http://localhost:8000/polls/34/results/」にアクセス する
      2. アプリケーションサーバには、リクエストURIとして 「polls/4/results/」が渡される。pollsアプリケーションのapps.pyに 「bname = polls」と設定されているので、pollsアプリケーションの urls.py の設定を参照する
      3. URIからアプリケーション名「polls/」を除いた文字列「34/results/」 が下記のpath関数の第1引数にマッチする。
        path('<int:question_id>/results/', views.results, name='results')
      4. 第2引数で指定された関数(views.pyのresults関数)を実行する。 このとき、path関数の第1引数の「<int:question_id>」がURIの「34」に マッチするので、名前付きパラメータ「question_id=34」がresult関数に 渡される。
    • プロジェクト名/urls.py

      それぞれのアプリケーションのURLディスパッチャの設定をプロジェクトに反映 する。既定の状態では、プロジェクト名/urls.py は以下のように設定されて いる。

      urlpatterns = [
          path('admin/', admin.site.urls),
      ]

      アプリケーションを追加したら、urlpatters に以下の設定を追加する。

          path('<アプリケーション名>/', include('<アプリケーション名>.urls')),
    参考文献

    はじめてのビュー作成 - はじめての Django アプリ作成、その 1

  6. ビューのテンプレートの配置と作成
    1. テンプレートの配置

      テンプレートは、アプリケーション名/templates/アプリケーション名 ディレクトリの下に配置する。

    2. テンプレートの記述
      • テンプレートの書き方の書き方

        以下のチュートリアルのセクションを参照のこと。

      • URL からのハードコーディングの排除

        テンプレートの中で以下のように記述したとする。

        <li><a href="/polls/{{ question_id }}/">{{ question.question_text }}</a></li>

        この場合、アプリケーションの名前空間が変わったり、urls.py で URL の パスの変更が行われた場合にビューの修正が発生する。以下のように 「{%url%}」を使用すれば、この問題を避けることができる。

        <li><a href="{% url '<名前空間>:<pathのname>' <パラメータ> ... %}">...</a></li>

        上記の設定の場合は、以下条件に一致する path関数の第一パラメータの値の 前に「アプリケーション名/」を加えたものが{%ul%}の結果となる。

        • urls.py の app_name に設定された値が名前空間と一致する アプリケーションの urls.py が対象
        • 上記の urls.py の urlpatters に設定された path関数の配列の中で、 name引数がpathのnameに一致する

        URLの文字列野中に「<...>」が含まれる場合は、{%url%}の2番目 以降の内容が順番に適用される。

        例えば、テンプレートに以下の記述をした場合は、{%ur%}の部分は以下の ように置き換わる。

        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
        1. urls.py の app_name に「polls」が設定されているファイル、つまり polls/urls.py の設定を参照する
        2. urlpatterns に設定された path 関数の中で、引数に「name=detail」が 設定されたものを探す
          path('<int:question_id>/', views.detail, name='detail'),
        3. 見つかったpath関数の設定の第1引数の値「<int:question_id>/」の 前に「アプリケーション名/」を加えたもの、つまり 「polls/<int:question_id>/」が URL となる
        4. 上記の URL に 「<>」で囲まれた部分があるので、この部分が {%url%}の2番目以降の引数の内容、つまり question.id の値に差し 変わる
        5. 最終的に {%url%}は、以下のURLに置き換わる。
          /pols/34/  (question.id の値が 34 の場合)
    参考文献
  7. ビューの実装

    ビューは、リクエストを受け取ってデータベースの検索や更新等の処理を実行し、 何らかの応答を返す処理である。

    • views モジュールのメソッドによるビューの実装

      アプリケーション名/views.py のURLディスパッチャから呼び出される関数を 作成し、以下のように記述する。

      from django.http import HttpResponse
      from django.template import loader
      
      def index(request):
          ...
          template = loader.get_template('<テンプレートファイルのパス>')
          context = {
              '<テンプレートの変数名>': <この関数の変数>
          }
          return HttpResponse(template.render(context, request))
      • テンプレートの取得

        loader.get_template 関数を実行してテンプレートをロードする。 引数の テンプレートファイルのパス にはtemplates ディレクトリからの 相対パスを指定する。

      • テンプレートによるレスポンスの作成

        template.render 関数を実行して応答で返すHTMLを作成する。 contextにはテンプレートに渡す引数をディクショナリで以下のように設定する。

        • テンプレートの変数名 - テンプレートの中で使用している変数の名前
        • この関数の変数 - 関数の中で使用している変数
      • テンプレート取得のショートカット

        django.shortcuts モジュールの render 関数 を使用して、template モジュールの loader.get_template 関数の呼び出しをショートカットする ことが可能である。

        from django.shortcuts import render
        
        def index(request):
            ...
            context = {
                '<テンプレートの変数名>': <この関数の変数>
            }
            return render(request, '<テンプレートファイルのパス>', context)
      • 404例外の送出

        以下のように モデルの objects.get 関数を呼び出す代わりに、 django.shortcuts モジュールの get_object_or_404 関数を使用することで、 検索条件に該当するデータが見つからない場合に404エラーを簡単に送出する ことができる。

        from django.shortcuts import render
        from django.shortcuts import get_object_or_404, get_list_or_404
        from django.http import HttpResponse
        
        def detail(request, question_id):
            question = get_object_or_404(Question, pk=question_id)
            context = {'question': question}
            return render(request, 'polls/detail.html', context)
      参考文献

      はじめての Django アプリ作成、その 3

    • 汎用ビュー

      汎用ビューは、データベースからモデルの一覧や1つのモデルを取得して テンプレートに渡す処理を汎用化したクラスである。これを使用すると、 djsnho.shortcuts モジュールの render関数の呼び出しを省略できる。

      汎用ビューを使用する手順は以下のとおり。

      1. アプリケーション名/views.py にビューのクラスを作成する。
        • Modelの一覧を表示する場合
          from django.views import generic
          from .models import Question
          
          class IndexView(generic.ListView):
              template_name = 'polls/index.html'
              context_object_name = 'latest_question_list'
          
              def get_queryset(self):
                  return Question.objects.order_by('-pub_date')[:5]

          [一覧を表示するビュークラスの例]

          説明

          • django.views.generic モジュールの ListView クラスを継承したクラスを 作成する
          • クラスの template_name 属性を上書きして、テンプレートファイルの パスを設定する
          • context_object_name 属性を上書きして、テンプレートに渡す変数名を 設定する
          • get_queryset 関数を実装して、テンプレート渡す変数の内容 (データベースからの検索結果)を返す
        • モデルの内容を表示する場合
          from django.views import generic
          from .models import Question
          
          class DetailView(generic.DetailView):
              model = Question
              template_name = 'polls/detail.html'

          [モデルの内容を表示するビュークラスの例]

          説明

          • model 属性にテンプレート渡すデータのクラスを指定する。テンプレート で参照する変数名は、クラス名を小文字に変換したものになる。
          • template_name 属性をオーバーライドして、テンプレートファイルの パスを指定する。
      2. urls.py の設定
        from django.urls import path
        from . import views
        
        app_name = 'polls'
        urlpatterns = [
            path('', views.IndexView.as_view(), name='index'),
            path('<int:pk>/', views.DetailView.as_view(), name='detail'),
            ...
        }

        path 関数の設定内容

        • 第1引数はリクエストURLの「アプリケーション名/」より後ろの部分を 指定する。モデルの内容を表示するビューの場合は、パラメータ名が「pk」 のパラメータにモデルの id が渡される。
        • 第2引数は、「ビュークラス.as_view()」を指定する。
        • name パラメータにこの設定を識別するための名前を設定する。
      参考文献

      汎用ビューを使う: コードが少ないのはいいことだ - はじめての Django アプリ作成、その 4

  8. フォームの作成
    • フォームの例

      チュートリアルの 簡単なフォームを書く に記載されているフォームの例は以下のとおり。

      <form action="{% url 'polls:vote' question.id %}" method="post">
      {% csrf_token %}
      <fieldset>
          <legend><h1>{{ question.question_text }}</h1></legend>
          {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
          {% for choice in question.choice_set.all %}
              <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
              <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
          {% endfor %}
      </fieldset>
      <input type="submit" value="Vote">
      </form>
    • フォームに関するトピックス

      二重送信を防止するためのテクニック。処理が成功したら、リダイレクトで 画面遷移する。

      django.urls モジュールの reverse 関数を呼び出すと、テンプレートの {%url%}と同様な URL を取得できる。

      from django.http import HttpResponseRedirect
      from django.urls import reverse
      
      def vote(request, question_id):
          question = get_object_or_404(Question, pk=question_id)
          ...
          return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
    参考文献

    簡単なフォームを書く - はじめての Django アプリ作成、その 4

自動テストの実行

Djangoの自動テストには、モデルのテストとビューのテストがある。どちらも アプリケーション名/test.py ファイルにテストのコードを記述する。

  • モデルのテストクラスの作成
    import datetime
    from django.test import TestCase
    from django.utils import timezone
    from .models import Question
    
    class QuestionModelTests(TestCase):
        def test_was_published_recently_with_future_question(self):
            '''
            was_published_recently() returns False for questions whose pub_date
            is in the future.
            '''
            time = timezone.now() + datetime.timedelta(days=30)
            future_question = Question(pub_date=time)
            self.assertIs(future_question.was_published_recently(), False)

    [モデルのテストクラスの実装例]

    • テストクラスは django.test.TestCase クラスを継承して作成する。
    • テスト結果の判定は、self.assertXXX メソッドを使用して判定する。
    • モデルのテストクラスの書き方については、以下のチュートリアルの記事を参照 すること。

      バグをあぶり出すためにテストを作成する

  • ビューのテストクラスの作成
    import datetime
    from django.test import TestCase
    from django.urls import reverse
    from django.utils import timezone
    from .models import Question
    
    def create_question(question_text, days):
        '''
        Create a question with the given 'question_text' and published the
        given number of 'days' offset to now (negative for questions published
        in the past, positive for questions that have yet to be published).
        '''
        time = timezone.now() + datetime.timedelta(days=days)
        return Question.objects.create(question_text=question_text, pub_date=time)
    
    class QuestionModelTests(TestCase):
        def test_future_question(self):
            '''
            Question with a pub_date in the future aren't displayed on
            the index page. 
            '''
            question = create_question(question_text='Future question.', days=30)
            response = self.client.get(reverse('polls:index'))
            self.assertContains(response, 'No polls are available.')
            self.assertQuerysetEqual(response.context['latest_question_list'], [])

    [ビューのテストクラスの実装例]

    • テストクラスの作成手順は、モデルのテストクラスと同じ
    • self.client.get 関数を実行してビューからの応答を取得して、その内容を テストする。
    • get関数に指定するURLは、django.urls パッケージの reverse関数を使用して、 urls.py の設定から URL を取得する。
    • ビューのテストクラスの書き方については、以下のチュートリアルの記事を参照 すること。

      新しいビューをテストする

  • テストの実行

    以下のコマンドを実行してテストを実行する。

    $ python manage.py test polls
参考文献

はじめての Django アプリ作成、その 5

静的ファイルの配置

  • 詳細は以下のチュートリアルの記事を参照のこと。

    はじめての Django アプリ作成、その 6

  • 静的ファイルは、アプリケーション名/static/アプリケーション名 ディレクトリの下に配置する。
  • スタイルシートの配置例
    • スタイルシートは、静的ファイルを配置するディレクトリの下に style.css の 名前で配置する。
    • テンプレートからのスタイルシートの参照は、テンプレートファイルの先頭に 以下のように記述する。
      {% load static %}
      <link rel="stylesheet" href="{% static 'polls/style.css' %}"/>
  • 背景画像の配置
    • 背景画像は、静的ファイルを配置するディレクトリの下に「images」 ディレクトリを作成し、その下に配置する。
    • スタイルシートの記述例
      li a {
          color: green;
      }
      
      body {
          background: white url("images/background.jpg");
      }
参考文献

https://docs.djangoproject.com/ja/4.1/intro/tutorial06/#writing-your-first-django-app-part-6


django topics

URL Dispatcher

URL ディスパッチャーの仕組み
  1. urls.py の urlpatterns 変数に、django.url モジュールの path 関数の 戻り値を配列で指定する。path 関数の引数は以下のとおり。
    • 第1引数 - URLのパターンを指定する。URLパターンに「Path コンバーター」を 含めることができる。
    • 第2引数 - viewモジュールの関数または汎用ビュークラスを指定する。
    • name 引数 - path 関数を識別するための名前を指定する。
  2. リクエストが Webアプリケーション(django)に届くと、urlpatterns の配列 要素の純にリクエストURLが path 関数の第1引数とマッチするかどうかを チェックし、マッチした場合は path 関数の第2引数で指定した view モジュールの 関数または汎用ビュークラスを呼び出す。

    URLのPathコンバーターにマッチした部分をpath関数の第2引数で指定した関数 または汎用ビューのメソッドから参照する方法は以下のとおり。

    • path 関数の第1引数に、「<Pathコンバーター名:引数名>」の形式で Pathコンバーターを指定した場合は、「引数名」の名前付きパラメータで参照 する
    • Pathコンバーターの指定で「Pathコンバーター名」を省略した場合(引数名のみ を指定した場合)は、「Pathコンバーター名」は「str」を指定したとみなされる
    • urls.py ファイルの urlpatters 変数に以下の設定をした場合について説明 する。

      urlpatterns = [
          path('<int:param1>/<str:param2>/<path:param3>', views.func1, name='detail'),
      ]

      リクエストURLの「アプリケーション名」より後ろの部分が 「123/abcXYZ/foo/bar?aaa=bbb」の場合は、viewsモジュールのfunc1関数が 呼び出される。このとき以下の名前付きパラメータで、それぞれのPath コンバーターに一致する部分を取得できる。

      • param1 - int Pathコンバーターにマッチした部分、即ち「123」
      • param2 - str Pathコンバーターにマッチした部分、即ち「abcXYZ」
      • param3 - path Pathコンバーターにマッチした部分、即ち「bar」。Path コンバーターはURLのパラメータにはマッチしないので、「?aaa=bbb」の 部分は param3 に含まれない
Path コンバーター

Path コンバーター(URLのパスにマッチするキーワード)には、以下のものがある。 ただし、URLのパラメータはマッチする文字列に含まれないことに注意すること。

  • <str> - 「/」以外の空でない文字列にマッチする
  • <int> - 正の整数にマッチする
  • <slug> - ASCII文字、数字、「-」、「_」にマッチする
  • <uuid> - UUIDにマッチする。アルファベットは小文字でハイフンを含まなければ ならない
  • <path> - 「/」を含む空でないすべての文字列にマッチする
参考文献

Path converters

本番環境リリース時の注意

  • settings.py の設定

    プロジェクト・コンテナ配下の プロジェクト名/settings.py ファイルを以下の ように設定する。

    DEBUG = False
    ALLOWED_HOSTS = ['<ホスト名>',...]
    • DEBUG

      既定値は「True」となっているが、「False」に設定する。「True」に設定されて いると、サーバーエラーが発生した場合に画面にデバッグ用の詳細情報が出力 されるため、本番では抑止すべきである。

    • ALLOWED_HOSTS

      HTTPリクエストヘッダの「Host」に設定された値が、ホスト名 と一致下場合 だけリクエストを受け付ける。

Requested setting INSTALLED_APPS, but settings are not configured.

このエラーが発生する場合は、下記のメッセージに従って対処する。

django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

具体的には、アプリケーションのパッケージディレク問い(チュートリアルの 場合は「polls」)にある「__init__.py」に以下の内容を記述する。

import os

if not os.environ.get('DJANGO_SETTINGS_MODULE'):
    import django
    os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'
    django.setup()

Topics

バージョンを固定して依存パッケージを再配置する

「pip install」でパッケージをインストールした後、成果物の配備などで依存 パッケージを再配置する場合に、インストールしたバージョンに固定したい場合は、 以下の手順を実施する。

  1. インストールしたパッケージのバージョンを確認する
    $ python -m pip list [--path <install-dir> | --user]

    「--path install-dir」を指定すると、指定したディレクトリーにインストール されたパッケージをリスト表示する。「--user」を指定すると、ユーザー・ ホームディレクトリーにインストールされたパッケージをリスト表示する。 これらのオプションを指定しない場合は、システム・ディレクトリーにインストール されたパッケージをリスト表示する。

    リスト表示されるパッケージは、インストール時に指定したパッケージの他に、 それに依存してインストールされたものも表示されるので、目的のパッケージを 探してバージョンを確認する。

  2. Requiremenets File の編集

    以下の形式でパッケージのバージョンを指定する。

    <package-name> == <version>

logging.basicConfig 関数の設定どおりにログが出力されない場合

logging.basicConfig 関数は、logger の handler が1つも存在しない場合のみ、 引数で設定した内容の handler を設定するように実装されている。従って、既に logger の handler が存在する場合は、何も処理が行われない。確実に logging.basicConfig の設定内容を反映させるには、以下のように logger の handerを全て削除した上で、logging.basicConfig 関数を呼び出す必要がある。

import logging

root = logging.getLogger()
if root.handlers:
    for handler in root.handlers:
        root.removeHandler(handler)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)

Maven Project

POM

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>ycookjp.myproject.python.packages</groupId>
  <artifactId>mypackage</artifactId>
  <!-- inheritated from parent
  -->
  <version>0.0.1-SNAPSHOT</version>
  <!-- end -->
  <packaging>pom</packaging>
  <description>Python My Package</description>
<!--
  <parent>
    <groupId>ycookjp.myproject.python</groupId>
    <artifactId>packages</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>
-->
  <name>Python My Package</name>
  <properties>
    <!-- Inheritated from parent project.
    -->
    <p.sep>${path.separator}</p.sep>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven-site.ver>3.12.1</maven-site.ver>
    <maven-project-info-reports.ver>3.4.1</maven-project-info-reports.ver>
    <maven-assembly.var>3.4.2</maven-assembly.var>
    <maven-resources.ver>3.3.0</maven-resources.ver>
    <maven-antrun.ver>3.1.0</maven-antrun.ver>
    <exec-maven-plugin.ver>3.1.0</exec-maven-plugin.ver>
    <maven-clean.ver>3.2.0</maven-clean.ver>
    <!-- end -->
    <PYTHONEXE>python3</PYTHONEXE>
    <PROJECT_NAME>${project.artifactId}</PROJECT_NAME>
    <MAIN_PATH>${project.build.directory}/build-tmp/src/main</MAIN_PATH>
    <TEST_PATH>${project.build.directory}/build-tmp</TEST_PATH>
    <PACKAGE_NAME>${PROJECT_NAME}</PACKAGE_NAME>
    <TEST_PY>test_all.py</TEST_PY>
    <TEST_REPORT_DIR>${project.build.directory}/site</TEST_REPORT_DIR>
  </properties>
  <!-- 
   - To generate test report, run "mvn clean test site"
   -->
  <build>
    <!-- Inheritated from parent project.
    -->
    <pluginManagement>
      <plugins>
        <!-- Basic report creation.  -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-site-plugin</artifactId>
          <version>${maven-site.ver}</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>${maven-project-info-reports.ver}</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <!-- end -->
    <plugins>
      <!-- cleaning files -->
      <plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>${maven-clean.ver}</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>${basedir}/</directory>
              <includes>
                <include>src/**/__pycache__/</include>
                <include>src/**/site-packages/</include>
              </includes>
              <followSymlinks>false</followSymlinks>
            </fileset>
          </filesets>
        </configuration>
      </plugin>

      <!-- copying resources -->
      <plugin>
        <artifactId>maven-resources-plugin</artifactId>
        <version>${maven-resources.ver}</version>
        <executions>

          <!-- copying main files -->
          <execution>
            <id>copy-main-files</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>copy-resources</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.directory}/build-tmp/src/main</outputDirectory>
              <resources>
                <resource>
                  <directory>${basedir}/src/main</directory>
                </resource>
              </resources>
            </configuration>
          </execution>

          <!-- copying test files -->
          <execution>
            <id>copy-test-files</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>copy-resources</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.directory}/build-tmp/tests</outputDirectory>
              <resources>
                <resource>
                  <directory>${basedir}/tests</directory>
                </resource>
              </resources>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <!-- Creating output directory for coverage report -->
      <plugin>
        <artifactId>maven-antrun-plugin</artifactId>
        <version>${maven-antrun.ver}</version>
        <executions>
          <execution>
            <id>mkoutdir-piplicenses</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>run</goal>
            </goals>
            <configuration>
              <target>
                <mkdir dir="${TEST_REPORT_DIR}" />
              </target>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <!-- Generating test reports -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.6.0</version>
        <executions>

          <!-- Installing main dependency modules -->
          <execution>
            <id>install-main-dependencies</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${PYTHONEXE}</executable>
              <workingDirectory>${MAIN_PATH}</workingDirectory>
              <commandlineArgs>-m pip install -t site-packages -r ${PACKAGE_NAME}/requirements.txt</commandlineArgs>
            </configuration>
          </execution>

          <!-- Installing pip-licenses -->
          <execution>
            <id>install-piplicenses</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${PYTHONEXE}</executable>
              <workingDirectory>${TEST_PATH}</workingDirectory>
              <commandlineArgs>-m pip install -t piplicenses-packages pip-licenses</commandlineArgs>
            </configuration>
          </execution>

          <!-- Generating dependencies report -->
          <execution>
            <id>execute-piplicenses</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${PYTHONEXE}</executable>
              <workingDirectory>${MAIN_PATH}/${PACKAGE_NAME}</workingDirectory>
              <commandlineArgs>-m piplicenses --format=html --output-file ${TEST_REPORT_DIR}/python-dependencies.html</commandlineArgs>
              <environmentVariables>
                <PYTHONPATH>${MAIN_PATH}/site-packages${p.sep}${TEST_PATH}/piplicenses-packages</PYTHONPATH>
              </environmentVariables>
            </configuration>
          </execution>

          <!-- Installing test dependency modules and pdoc3 -->
          <execution>
            <id>install-test-dependencies</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${PYTHONEXE}</executable>
              <workingDirectory>${TEST_PATH}</workingDirectory>
              <commandlineArgs>-m pip install -t site-packages -r tests/requirements.txt pdoc3</commandlineArgs>
            </configuration>
          </execution>

          <!-- Generating API document -->
          <execution>
            <id>python-apidocs</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${PYTHONEXE}</executable>
              <workingDirectory>${MAIN_PATH}</workingDirectory>
              <commandlineArgs>-m pdoc --html -o ${TEST_REPORT_DIR}/apidocs --config show_source_code=False --force ${PACKAGE_NAME}</commandlineArgs>
              <environmentVariables>
                <PYTHONPATH>${MAIN_PATH}/site-packages${p.sep}${TEST_PATH}/site-packages</PYTHONPATH>
              </environmentVariables>
            </configuration>
          </execution>

          <!-- Generating test spec document -->
          <execution>
            <id>python-testspecs</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${PYTHONEXE}</executable>
              <workingDirectory>${TEST_PATH}</workingDirectory>
              <commandlineArgs>-m pdoc --skip-errors --html -o ${TEST_REPORT_DIR}/testspecs --config show_source_code=False --force tests</commandlineArgs>
              <environmentVariables>
                <PYTHONPATH>${MAIN_PATH}/site-packages${p.sep}${TEST_PATH}/site-packages</PYTHONPATH>
              </environmentVariables>
            </configuration>
          </execution>

          <!-- Collecting coverage data and generating test result report -->
          <execution>
            <id>python-coverage</id>
            <phase>test</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${TEST_PATH}/site-packages/bin/coverage</executable>
              <workingDirectory>${MAIN_PATH}</workingDirectory>
               <arguments>
                <argument>run</argument>
                <argument>--branch</argument>
                <argument>--source</argument>
                <argument>${MAIN_PATH}/${PACKAGE_NAME}</argument>
                <argument>${TEST_PATH}/tests/${TEST_PY}</argument>
              </arguments>
              <environmentVariables>
                <PYTHONPATH>${MAIN_PATH}/site-packages${p.sep}${MAIN_PATH}${p.sep}${TEST_PATH}/site-packages${p.sep}${TEST_PATH}/tests</PYTHONPATH>
              </environmentVariables>
              <successCodes>
                <!-- 
                  Continue generating reports even if unit test fails.
                 -->
                <successCode>0</successCode>
                <successCode>1</successCode>
              </successCodes>
            </configuration>
          </execution>

          <!-- Generating coverage report -->
          <execution>
            <id>python-coverage-html</id>
            <phase>test</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <executable>${TEST_PATH}/site-packages/bin/coverage</executable>
              <workingDirectory>${MAIN_PATH}</workingDirectory>
              <arguments>
                <argument>html</argument>
                <argument>--directory=${TEST_REPORT_DIR}/htmlcov</argument>
              </arguments>
              <environmentVariables>
                <PYTHONPATH>${MAIN_PATH}/site-packages${p.sep}${MAIN_PATH}${p.sep}${TEST_PATH}/site-packages${p.sep}${TEST_PATH}/tests</PYTHONPATH>
              </environmentVariables>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <artifactId>maven-antrun-plugin</artifactId>
        <version>${maven-antrun.ver}</version>
        <executions>
          <!-- Patching HtmlTestRunner -->
          <execution>
            <id>patching-htmltestrunner</id>
            <phase>process-test-classes</phase>
            <goals>
              <goal>run</goal>
            </goals>
            <configuration>
              <failOnError>false</failOnError>
              <target>
                <!-- Patching HtmlTestReport package -->
                <echo>Patching ${TEST_PATH}/site-packages/HtmlTestRunner/result.py</echo>
                <replaceregexp match='^[ \t]*length = self._count_relevant_tb_levels\(tb\)$' replace="" encoding="UTF-8" byline="true" flags="g">
                  <fileset dir="${TEST_PATH}/site-packages/HtmlTestRunner" includes="result.py"/>
                </replaceregexp>
                <replaceregexp match='msg_lines = traceback.format_exception\(exctype, value, tb, length\)' replace="msg_lines = traceback.format_exception(exctype, value, tb)" encoding="UTF-8" byline="true" flags="g">
                  <fileset dir="${TEST_PATH}/site-packages/HtmlTestRunner" includes="result.py"/>
                </replaceregexp>
              </target>
            </configuration>
          </execution>

          <!-- Copying test reports from build-tmp and replacing file path -->
          <execution>
            <id>copy-testreports</id>
            <phase>site</phase>
            <goals>
              <goal>run</goal>
            </goals>
            <configuration>
              <failOnError>false</failOnError>
              <target>
                <!-- Copying test reports from build-temp directory -->
                <mkdir dir="${TEST_REPORT_DIR}" />
                <copy todir="${TEST_REPORT_DIR}">
                  <fileset dir="${project.build.directory}/build-tmp/target/site" includes="**/*" />
                </copy>
                <!-- Replacing file path in test reports -->
                <replaceregexp match='[-_#$%()+=@0-9a-zA-Z\^`\[\]{};:\/\\]*[\/\\]*target[\/\\]*build-tmp[\/\\]*' replace="" encoding="UTF-8" byline="true" flags="g">
                  <fileset dir="${TEST_REPORT_DIR}/htmlcov" includes="**/*"/>
                  <fileset dir="${TEST_REPORT_DIR}/test-report" includes="**/*"/>
                  <fileset dir="${TEST_REPORT_DIR}/apidocs" includes="**/*"/>
                  <fileset dir="${TEST_REPORT_DIR}/testspecs" includes="**/*"/>
                </replaceregexp>
                <replaceregexp match='[-_#$%()+=@0-9a-zA-Z\^`\[\]{}]*_target_build-tmp_' replace="" encoding="UTF-8" byline="true" flags="g">
                  <fileset dir="${TEST_REPORT_DIR}/htmlcov" includes="**/*"/>
                </replaceregexp>
                <move todir="${TEST_REPORT_DIR}/htmlcov">
                  <fileset dir="${TEST_REPORT_DIR}/htmlcov" />
                  <mapper type="regexp" from="^.*_target_build-tmp_(.*)" to="\1" />
                </move>
              </target>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <!-- Archiving source script files -->
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>${maven-assembly.var}</version>
        <configuration>
          <descriptors>
            <descriptor>src/assenbly/distribution.xml</descriptor>
          </descriptors>
          <finalName>${project.artifactId}-${project.version}</finalName>
          <outputDirectory>${TEST_REPORT_DIR}</outputDirectory>
          <appendAssemblyId>false</appendAssemblyId>
        </configuration>
        <executions>
          <execution>
            <id>distribution</id>
            <phase>site</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <!-- Inheritated from parent project.
  -->
  <reporting>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-project-info-reports-plugin</artifactId>
        <version>${maven-project-info-reports.ver}</version>
      </plugin>
    </plugins>
  </reporting>
  <!-- end -->
</project>

[pom.xml]

ファイルの配置

<basedir>
  |- pom.xml
  `- src
      |- assembly
      |   `- distribution.xml
      |- main
      |   `- <package-name>
      |       |- <source-scripts>
      |       `- requirements.txt
      |- test
      |   `- tests
      |       |- <test-scripts>
      |       |- test_all.py
      |       |- test_all.yml
      |       '- requirements.txt
      `- site
          `- site.xml