ラベル Python の投稿を表示しています。 すべての投稿を表示
ラベル Python の投稿を表示しています。 すべての投稿を表示

2018年3月19日月曜日

openpyxlで行のグルーピング

PythonでExcelを読み書きするライブラリの1つとして、openpyxlがあります。
公式サイトには列のグルーピングについては記載されていましたが、行のグルーピングについては記載されていませんでした。
ソースを参照しながら工夫したらどうにかできたので公開します。
今後公式から行のグルーピングについてアナウンスされるかもしれませんので、ご留意ください。
以下、ソースです。

2014年9月29日月曜日

行指向ファイル処理フレームワーク

業務でファイルを一行ずつ読み込んで処理するプログラムをコーディングしているのですが、提供された設計書のフローチャートに従って実装を進めていたら、脳みそがぐちゃぐちゃにかき回されるようないらだちを覚えました。
"ファイルを一行ずつ読み込んで処理する"という要件は、あちこちで需要がありそうなので、モデルを考えてみました。 Process、FileFinder、FileHandler、LineHandlerの4つのクラスで構成します。

2014年7月14日月曜日

ちょっとしたツールを作るのに便利なPythonのcmdモジュールを使うための3ステップ

シンプルなコマンドラインツールが必要なとき、Pythonのcmdモジュールが有力な選択肢になります。ツールを実装するために必要な3ステップを紹介します。
  1. cmd.Cmdクラスを継承したクラスを作る
  2. do_XXXメソッドを定義する
  3. cmdloopメソッドを呼ぶ
cmd.Cmdクラスを継承したクラスを作る
特に言うべきことはありません。普通に継承するだけです。
class HelloCmd(cmd.Cmd):
...

do_XXXメソッドを定義する
メソッド名の"XXX"がコマンドになります。
パラメータを受け取るためのargパラメータがポイントです。
    def do_hello(self, arg):
        print 'hello, ' + str(arg)

cmdloopメソッドを呼ぶ
クラスをインスタンス化し、cmdloopを呼ぶとREPLがスタートします。
終了するにはCtrl-Cで落とします。
>>> HelloCmd().cmdloop()
(Cmd) hello Taro
Hello, Taro
(Cmd) Traceback (most recent call last):
  File "", line 1, in 
  File "/usr/lib/python2.7/cmd.py", line 130, in cmdloop
    line = raw_input(self.prompt)
KeyboardInterrupt

以下Tips。必須ではありませんが、知っておくと便利です。

2014年5月5日月曜日

unittest.mockの簡単な紹介

単体テストに欠かせないツールのひとつに、モックがあります。
Pythonにも3.3からモックモジュールが追加されました。
unittest.mockといいます。今回はこのモジュールの簡単な使い方を紹介します。

モックが必要になる場面として、システム日付の取得があります。
例えば、当日が閏日かどうか判定する関数を考えてみましょう。
なにも考えずに関数を実装すると、次のようになると思います。
def is_leap():
    d = datetime.date.today()
    if 2 == d.month and 29 == d.day:
        print(str(d) + ' is LEAP')
        return True
    else:
        print(str(d) + ' is NOT LEAP')
        return False

このままではOSの時計を設定しないと試験ができないので、
日付をパラメータとして渡すようにします。
def is_leap(d):
    if 2 == d.month and 29 == d.day:
        print(str(d) + ' is LEAP')
        return True
    else:
        print(str(d) + ' is NOT LEAP')
        return False

日付を注入できるようにしたことで、テストしやすくなりました。
例えば、このようにします。
assert True == is_leap(datetime.date(2012, 2, 29))
assert False == is_leap(datetime.date(2012, 3, 1))

unittest.mockを使うと、次のような感じになります。
d = unittest.mock.MagicMock()
d.month = 2
d.day = 29
assert True == is_leap(d)

モックを使わないほうが単純です。こういう場合はdateオブジェクトを使ったほうがいいでしょう。
さて、現実には全てのコードを自由に変更できるとは限りません。
たとえそんな場合でも、unittest.mock.patchを呼ぶことで、対応できる場合があります。
is_leap関数を修正せずに次のようにします。
with unittest.mock.patch('datetime.date') as dt:
    dt.today().month = 2
    dt.today().day = 29
    assert True == is_leap()

datetime.date.today()メソッドが返すオブジェクトをモックオブジェクトと置き換え、
属性を設定することでテストをパスしています。
確かにこの方法でも試験はできますが、日付をパラメータ化するほうがベターだと思います。

他にも、unittest.mockにはメソッドの呼び出しを記録する機能もあります。
詳しいことはオフィシャルのドキュメントを参照してください。

参考:

2014年3月24日月曜日

PythonでEvernoteへメールを送る - 添付ファイル追加

PythonでEvernoteへメールを送る」で載せたプログラムへ、ファイル添付機能を追加しました。
合わせてクラス設計を見直しました。

# -*- coding: utf-8 -*-

# ---- CONFIGURATIONS ----
EVERNOTE_MAIL_ADDRESS = 'evernote_mail_address'
FROM_ADDRESS = 'from_address'
SMTP_HOST = 'smtp_host'
SMTP_PORT = 25
# ---- CONFIGURATIONS ----

import datetime
import mimetypes
import optparse
import os.path
import smtplib
import sys

from email import encoders
from email.header import Header
from email.mime.audio import MIMEAudio
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

class EverMailClient(object):
 def __init__(self, smtp):
  assert isinstance(smtp, smtplib.SMTP)
  self._smtp = smtp
  
 def send(self, mail):
  assert isinstance(mail, EverMail)
  msg = mail.as_MIME()
  mail_from = msg['From']
  mail_to = msg['To']
  self._smtp.sendmail(mail_from, mail_to, msg.as_string())


class EverMail(object):
 def __init__(self, title, from_address, to_address, body, attached_files):
  assert isinstance(title, EverMailTitle)
  assert isinstance(body, EverMailBody)
  self._title = title
  self._from_address = from_address
  self._to_address = to_address
  self._body = body
  self._attached_files = attached_files
  
 def as_MIME(self):
  msg = MIMEMultipart()
  msg['Subject'] = Header(str(self._title), 'utf-8')
  msg['From'] = self._from_address
  msg['To'] = self._to_address
  msg.attach(self._body.as_MIME())
  for attached_file in self._attached_files:
   msg.attach(attached_file.as_MIME())
  return msg


class EverMailTitle(object):
 def __init__(self, title, notebook, tags):
  self._title = title if title else str(datetime.datetime.now())
  self._notebook = notebook if notebook else ''
  self._tags = tags if tags else []
  
 def __str__(self):
  result = self._title
  if 0 < len(self._notebook):
   result += ' @' + self._notebook
  for tag in self._tags:
   result += ' #' + tag
  return result


class EverMailBody(object):
 def __init__(self, stdin):
  assert hasattr(stdin, 'read')
  self._stdin = stdin
  
 def as_MIME(self):
  body = sys.stdin.read()
  msg = MIMEText(bytes(body))
  msg.set_charset('utf-8')
  return msg

  
class AttachedFile(object):
 def __init__(self, path):
  self._path = path
  
 def as_MIME(self):
  ctype, encoding = mimetypes.guess_type(path)
  if ctype is None or encoding is not None:
   ctype = 'application/octet-stream'
  maintype, subtype = ctype.split('/', 1)
  msg = None
  with open(self._path, 'rb') as f:
   if maintype == 'text':
    msg = MIMEText(f.read(), _subtype=subtype)
   elif maintype == 'image':
    msg = MIMEImage(f.read(), _subtype=subtype)
   elif maintype == 'audio':
    msg = MIMEAudio(f.read(), _subtype=subtype)
   else:
    msg = MIMEBase(maintype, subtype)
    msg.set_payload(f.read())
    encoders.encode_base64(msg)
  file_name = os.path.basename(self._path) 
  msg.add_header('Content-Disposition', 'attachment', filename=file_name)
  return msg


class EverMailOptionParser(optparse.OptionParser):
 def __init__(self):
  optparse.OptionParser.__init__(self)
  self.add_option('--notebook', '-n', action='store')
  self.add_option('--tag', '-t', action='append')

    
def create_smtp_client():
 smtp = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
 smtp.set_debuglevel(1)
 return smtp
 
 
if __name__ == '__main__':
 option_parser = EverMailOptionParser()
 (options, args) = option_parser.parse_args()
 note_title = args[0] if args and 0 < len(args) else ''
 title = EverMailTitle(note_title,
                       options.notebook,
                       options.tag)
 body = EverMailBody(sys.stdin)
 attached_files = []
 if 2 <= len(args):
  attached_files = [AttachedFile(path) for path in args[1]]
 mail = EverMail(title,
                 FROM_ADDRESS,
                 EVERNOTE_MAIL_ADDRESS,
                 body,
                 attached_files)
 
 smtp = None
 try:
  smtp = create_smtp_client()
  client = EverMailClient(smtp)
  client.send(mail)
 finally:
  if smtp: smtp.close()

複数ファイルを添付するとうまくいかないところが問題です。
また時間のあるときに対応したいと思います。

2014年3月17日月曜日

PythonでEvernoteへメールを送る

Evernoteへメールを送ることでノートを追加できるのはご存知かと思います。
最近、古いネットブックをUSBブートのLinuxで使用しています。ネットブック上で作成したファイルをEvernoteへアップロードしたいと思うことがあるのですが、ブラウザから操作するのは厳しいと感じています。
Pythonからメールでファイルを送るようにすればいいのではないかと思い、標準入力から読み込んだ内容をEvernoteへ送るプログラムを書きました。

プログラム
# -*- coding: utf-8 -*-
# ever_mail.py

# ---- CONFIGURATIONS ----
EVERNOTE_MAIL_ADDRESS = 'evernote_mail_address'
FROM_ADDRESS = 'from_address'
SMTP_HOST = 'smtp._host'
SMTP_PORT = 25
DEBUG = True
# ---- CONFIGURATIONS ----

import datetime
import email.header
import optparse
import smtplib
import sys

from email.mime.text import MIMEText

class EverMailClient(object):
    def __init__(self, smtp):
        assert isinstance(smtp, smtplib.SMTP)
        self._smtp = smtp

    def send(self, msg):
        assert isinstance(msg, MIMEText)
        mail_from = msg['From']
        mail_to = msg['To']
        self._smtp.sendmail(mail_from, mail_to, msg.as_string())


class EverMailTitle(object):
    def __init__(self, title, notebook, tags):
        self._title = title if title else str(datetime.datetime.now)
        self._notebook = notebook if notebook else ''
        self._tags = tags if tags else []

    def __str__(self):
        result = self._title
        if 0 < len(self._notebook):
            result += ' @' + self._notebook
        for tag in self._tags:
            result += ' #' + tag
        return result


class EverMailOptionParser(optparse.OptionParser):
    def __init__(self):
        optparse.OptionParser.__init__(self)
        self.add_option('--notebook', '-n', action='store')
        self.add_option('--tag', '-t', action='append')


def create_smtp_client():
    smtp = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
    if DEBUG:
        smtp.set_debuglevel(1)
    return smtp

def create_message_from_stdin(title):
    assert isinstance(title, EverMailTitle)
    body = sys.stdin.read()
    msg = MIMEText(bytes(body))
    msg['Subject'] = email.header.Header(str(title), 'utf-8')
    msg['From'] = FROM_ADDRESS
    msg['To'] = EVERNOTE_MAIL_ADDRESS
    msg.set_charset('utf-8')
    return msg


if __name__ == '__main__':
    option_parser = EverMailOptionParser()
    (options, args) = option_parser.parse_args()
    note_title = args[0] if args and 0 < len(args) else ''
    title = EverMailTitle(note_title,
                          options.notebook,
                          options.tag)
    msg = create_message_from_stdin(title)
    smtp = None
    try:
        smtp = create_smtp_client()
        client = EverMailClient(smtp)
        client.send(msg)
    finally:
        if smtp: smtp.close()


使用例
$ cat your_important_file | python ever_mail.py TITLE -nNOTEBOOK -tTAG -tTAG

工夫したところは、optparseモジュールを使ってオプションの解析を実装したところです。
暇があれば、画像の添付なども対応したいと思います。

参考
15.5. optparse — コマンドラインオプション解析器
Eメールを送信するだけでEvernoteに簡単送信!

2014年3月3日月曜日

Pythonで単体テストをするには

pythonでunittestをするには」にて、
しかし、複数のテストファイルを作成した後でまとめてテストを実行する手段がないようなので、以下のようなコードを書いてみました。
という記載がありますが、Python 2.6にてまとめてテストを実行する手段が追加されましたので、勝手にフォローさせていただきます。動作確認にはPython 3.3を使用しました。

テストプログラム
テストプログラムのディレクトリ構成とプログラムは以下のとおりです。
test
├test1.py
├test2.py
└testsub
 ├__init__.py
 └testsub1.py

test/test1.py
# -*- coding: utf-8 -*-

import unittest

class TestCase1(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()

class TestCase2(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()

suite = unittest.TestSuite()
suite.addTest(TestCase1('test_success'))
suite.addTest(TestCase2('test_success'))


test/test2.py
# -*- coding: utf-8 -*-

import unittest

class TestCase1(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()



test/testsub/__init__.py
空のファイルです。testsubをモジュールとして認識させるために必要です。

test/testsub/testsub1.py
# -*- coding: utf-8 -*-

import unittest

class TestCase1(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()



フォルダ配下のテストをすべて実行する
pythonの-mオプションのパラメータとしてunittestを渡します。詳細な出力を得るために、-vオプションも追加します。
~/py/test $ python3 -m unittest -v
test_fail (test1.TestCase1) ... FAIL
test_success (test1.TestCase1) ... ok
test_fail (test1.TestCase2) ... FAIL
test_success (test1.TestCase2) ... ok
test_fail (test2.TestCase1) ... FAIL
test_success (test2.TestCase1) ... ok
test_fail (testsub.testsub1.SubTestCase1) ... FAIL
test_success (testsub.testsub1.SubTestCase1) ... ok

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (test1.TestCase2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/test1.py", line 17, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (test2.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/test2.py", line 10, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (testsub.testsub1.SubTestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/testsub/testsub1.py", line 10, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 8 tests in 0.004s

FAILED (failures=4)


モジュールを指定してテストする
上のオプションに加え、モジュール名を追加するとモジュールのテストになります。
~/py/test $ python3 -m unittest -v test1
test_fail (test1.TestCase1) ... FAIL
test_success (test1.TestCase1) ... ok
test_fail (test1.TestCase2) ... FAIL
test_success (test1.TestCase2) ... ok

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (test1.TestCase2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 17, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=2)


クラスを指定してテストする
さらにクラス名を追加します。
~/py/test $ python3 -m unittest -v test1.TestCase1
test_fail (test1.TestCase1) ... FAIL
test_success (test1.TestCase1) ... ok

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)


テストメソッドを指定してテストする
さらにメソッド名を追加します。
~/py/test $ python3 -m unittest -v test1.TestCase1.test_fail
test_fail (test1.TestCase1) ... FAIL

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)


テストスイートを指定してテストする
テストスイートを指定することもできます。要領はクラスやメソッドを指定する時と同様です。
~/py/test $ python3 -m unittest -v test1.suite
test_success (test1.TestCase1) ... ok
test_success (test1.TestCase2) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK



一括して実行できるだけでなく、任意のテストメソッドを選択することもできるので、テンポよく開発が進められると思いました。