2019/10/08

Python Tips: Python で BOM つき UTF-8 のファイルを探したい

Python で BOM 付き UTF-8 でエンコードされたファイルを特定のディレクトリの中から探す方法についてです。

UTF-8 の BOM とは何ぞやというお話は別記事で述べているので興味のある方はそちらをご覧ください:


ここでは特定のディレクトリの下に BOM 付きの UTF-8 と BOM なしの UTF-8 、そしてその他のエンコーディングのファイルも混在している状況を想定しています。その中から BOM 付き UTF-8 のファイルを全件リストアップしたいイメージです。

上の記事で述べたとおり、 BOM 付きの UTF-8 かどうかを見分けるには 'utf-8' で読み込んで置いて先頭の文字が BOM を表す '\ufeff' と一致するかどうかをチェックすれば OK です。そのため、あとは特定のディレクトリ以下のファイルを順に見ていくだけです。ファイルを見ていくには pathlib.Path.glob() あたりを使うのがシンプルでかんたんだと思います。

以下一連のコードです。

"""Locates files with utf-8 with BOM in a directory.
"""

import sys
from pathlib import Path

ENCODING = 'utf-8'
BOM = '\ufeff'


def main():
    current_dir = Path('.')
    for path in current_dir.glob('**/*.*'):
        if path.is_dir():
            continue

        try:
            if has_bom(path):
                print(path)
        except UnicodeDecodeError:
            print(f'{path} is not encoded with {ENCODING}.', file=sys.stderr)
            continue


def has_bom(path: Path) -> bool:
    try:
        with path.open(encoding=ENCODING) as f:
            line_first = f.readline()
            if line_first.startswith(BOM):
                return True
    except UnicodeDecodeError as e:
        raise

    return False


if __name__ == '__main__':
    main()

UTF-8 で読み込めないファイル(= UTF-8 以外のエンコーディングのファイル)があった場合は UnicodeDecodeError が上がるので適宜処理をします。ここでは対象のファイル名を含むエラー文を標準エラーに出力しています。

以上です。

2019/08/21

Python Tips:スーパークラスのメソッドをオーバーライドできているか確認したい

今回は スーパークラスのメソッドをオーバーライドをできているか確認する方法 についてです。

Python には言語そのものの機能として「特定のメソッド定義が祖先クラスのメソッドの正しいオーバーライドであること」を確約する方法が(私が知るかぎり)ありません。

そのため、他人が作ったクラス(ライブラリやフレームワークが提供するクラス)を継承したクラスを作成しメソッドをオーバーライドするときには、メソッド名のタイポに十分に注意する必要があります。オーバーライドしたつもりでできていないと思わぬバグに苦しめられ多くの時間を浪費してしまうこともあります。また、ライブラリのバージョンアップ等によって、継承元クラスのメソッドがいつの間にかなくなってしまい、オーバーライドがいつからかうまく行かなくなっていた、ということもあります。

そんなときに便利なのが、メソッド定義時に「スーパープラスのメソッドを正しくオーバーライドできていること」を確認する仕組みを入れておくことです。

具体的には、次のようなデコレータを定義します。

# f-string を使用しているため、動かすには Python 3.6 以上が必要です。

def overrides(klass):
    def check_super(method):
        method_name = method.__name__
        msg = f'`{method_name}()` is not defined in `{klass.__name__}`.'
        assert method_name in dir(klass), msg

    def wrapper(method):
        check_super(method)
        return method

    return wrapper

このデコレータ overrides は次のように使用することができます。

class A:
    def method1(self):
        pass


class B(A):
    @overrides(A)
    def method1(self):
        pass

クラス B はクラス A を継承しています。そして、クラス B のメソッド method1() はクラス Amethod1() をオーバーライドしています。

このコードはエラーなく動いてくれます。

一方、次のクラス C の場合は C の定義時( compile time )にエラーが発生します。

class A:
    def method1(self):
        pass


class C(A):
    @overrides(A)
    def method2(self):
        pass

クラス C はクラス A を継承しています。ただし、クラス C ではクラス A のメソッド method1() をオーバーライドしたつもりがタイポにより method2() という名前になってしまっています。

この C のコードを実行すると、次のようなエラーが発生し、オーバーライドの間違いを早期に教えてくれます。

Traceback (most recent call last):
  File "xxx.py", line n, in 
    class C(A):
  File "xxx.py", line n, in C
    @overrides(A)
  File "xxx.py", line n, in wrapper
    check_super(method)
  File "xxx.py", line 14, in check_super
    assert method_name in dir(klass), msg
AssertionError: `method2()` is not defined in `A`. 

以上です。

ちなみに、今回のものはデコレータを使ったアプローチでしたが、これと同じことを実現する他の方法としては「メタクラスを使った方法」や「 __init_subclass__() を使った方法」等があるようです。

参考:

2019/03/27

Python Tips: pip でパッケージの開発版を利用したい

pip で Python パッケージの開発版を利用する方法についてです。

ここで「パッケージ」というのは、コマンド pip (または python3 -m pip )でインストールできる distribution packages (配布パッケージ)のことを指しています。

また、「開発版」というのは、バージョンがまだ付けられていない最新のコードという意味です。例えば、この記事を書いてる時点で Django の最新バージョンは 2.1.7 ですが、開発版はそこからおよそ 50 〜 100 コミット進んでいます。

早速結論ですが、次のように pip install コマンドの引数を git+[リポジトリの URL] とすれば OK です。

$ pip install git+[リポジトリの URL]

例えば Django の場合だとリポジトリが GitHub にあるので次のようにします。

$ pip install git+https://github.com/django/django.git

本記事執筆時に確認したかぎりは、 GitHub の場合は末尾の .git を省略することもできます。

$ pip install git+https://github.com/django/django

特定のリビジョンのコードを使いたい場合は、次のように末尾に @[コミットハッシュ] を追加すれば OK です。一意に特定できれば、ハッシュはすべて指定しなくてもよいようです。

$ # https://github.com/django/django/commit/d26b2424437dabeeca94d7900b37d2df4410da0c を利用する
$ pip install git+https://github.com/django/django@d26b2424437

@~ は、コミットハッシュの他にもタグやブランチで指定したい場合にも使用することができます。

コードをビルドせずにそのまま利用したい場合は editable mode の -e オプションを併用します。ただし、 editable mode を使用する場合は、次のように末尾に #egg=[パッケージ名] で名前を指定してやる必要があります。指定しなかった場合はその旨のエラーメッセージが表示されるだけでコマンドが終了します。

$ pip install -e git+https://github.com/django/django.git#egg=django

開発版を入れたパッケージをアンインストールしたいときは、通常の場合と同じように単純にパッケージ名を指定すれば OK です。

$ pip uninstall django

以上です。

昨今 VCS といえば Git で、私が実際に利用しているのは Git だけなのでここで Git を例にあげましたが、 Git の他にも Mercurial ・ Bazaar ・ Subversion がサポートされているとのこと。私は試していませんが、それぞれプリフィックスは次のとおりです。

  • Mercurial: hg+
  • Bazaar: bzr+
  • Subversion: svn+

実際に Git 以外の VCS を利用したいときは PyPI のドキュメント を確認してから利用するようにしてください。

ちなみに、 pip + venv なパッケージ管理ツール Poetry の場合は、専用の --git というオプションが用意されており、次のような形で開発版を利用できるようになっています。 pip よりも少しわかりやすいですね。

$ poetry add --git=https://github.com/django/django django

ちなみに --git オプションを使ってインストールされたパッケージは pyproject.toml 内で次のように表現されます(動作確認は Poetry 0.12.11 で行いました)。

django = {git = "https://github.com/django/django"}

というわけで pip でパッケージの開発版を利用する方法についてでした。

参考