dorapon2000’s diary

忘備録的な。セキュリティとかネットワークすきです。

flake8のE125とE129の扱いについて

pythonのコーディング規約としてPEP8はとても有名です.そして,pycodestyleはコードがPEP8に準拠しているかチェックし,不適切であれば対応したエラーコードを示すツールです.pythonを使っている方にはお馴染みかもしれないflake8でも,内部でpycodestyleを使っています.

問題はflake8(正確にはpycodestyle)のエラーコードE129と大元のPEP8に矛盾があるのではないかということです.また,E125とE129の関係も含めて,調べてみました.

E125とは

Continuation line with same indent as next logical line

継続行のインデントレベルが次に続く論理行と同じインデントレベルになっている

# bad
if user is not None and user.is_admin or \
    user.name == 'Grant':
    blah = 'yeahnah'


# good
if user is not None and user.is_admin or \
        user.name == 'Grant':
    blah = 'yeahnah'

E129とは

Visually indented line with same indent as next logical line

視覚的に統一するためのインデントが次に続く論理行と同じインデントレベルになっている

# bad
if (row < 0 or module_count <= row or
    col < 0 or module_count <= col):
    raise Exception("%s,%s - %s" % (row, col, self.moduleCount))

# good
if (row < 0 or module_count <= row or
        col < 0 or module_count <= col):
    raise Exception("%s,%s - %s" % (row, col, self.moduleCount))

PEP8におけるインデントの例

if 文の条件部分が、複数行にまたがって書かなければならないくらい十分に長い場合があります。この場合、2文字のキーワード(つまり、 if) の後にスペースを一つ置き、開き括弧を置くと、2行目以降の条件部分は通常スペース4つ分インデントされることになります。 if 文の中でネストされるインデントされたコードも通常スペース4つ分インデントされるので、ネストされたコードの固まりと条件部分が見た目上区別がつかなくなってしまう可能性があります。 この PEP は、 if 文に含まれるネストされたコードの部分と、継続された条件部分を区別するかどうか(またはどうやって区別するか)については立場を示しませんが、許容できるやり方はいくつかあります:

# 追加のインデントをしない
if (this_is_one_thing and
    that_is_another_thing):
    do_something()

# シンタックスのハイライトをサポートするエディタで区別するため
# コメントを追加する
if (this_is_one_thing and
    that_is_another_thing):
    # 両方の条件がtrueなので、処理を調整可能
    do_something()

# 継続された行の条件をインデントする
if (this_is_one_thing
        and that_is_another_thing):
    do_something()

https://pep8-ja.readthedocs.io/ja/latest/#id5

要約すると,if文の条件文と直後にある論理文のインデントが揃うとみづらいけど,直してもいいし直さなくてもいいよ.ということです.

E129とPEP8の矛盾

E129では,if文の条件分と直後の論理文のインデントが揃うのはNGとしていますが,PEP8ではNGとしていません.ここで矛盾が発生します.どちらを信じればいいのでしょう.とりあえず,どちらの制約も守りたいというならば,「# 継続された行の条件をインデントする」のやり方をすれば解決します.しかし,個人的には嫌いです.

if (is_aaaaaaaaaaaaa
    and is_bbbbbbbbbbbb
        and is_cccccccccccccc):
    do_something()

条件文が3行に渡る時,最後だけインデントが深くなるのが気になって夜も寝られません.

矛盾が発生した経緯

github.com

上記のIssueの中で説明されていました.

sigmavirus24 commented on 3 Feb 2016

Please understand that the document is frequently updated and this particular section that you've emphasized was written after E129 was created. Also, this tool is being rebranded to avoid any further confusion of its checks with PEP-0008 the document.

以前はPEP8に準拠していたE125が,PEP8の更新にともない矛盾する形になったため,矛盾するif文の部分だけE125から分離してE129として作成し,矛盾が気になるならユーザ自身でE129を無効化できるようにした.ということらしいです.コミュニティはちゃんと矛盾を認識して対応させていたのですね.なぜ,pycodestyleはデフォルトでE129を無効にしないのかという疑問は,以下で回答されています.

sigmavirus24 commented on 3 Feb 2016

We add very few checks per minor release and try to disable very few by default. If we keep shifting checks into disabled by default or on by default and adding new checks, we're making the tool actively user-hostile. When people run pep8/pycodestyle/flake8 in their CI to check for style consistency and your example code stops returning a warning when people are expecting it to (and feel that the style pycodestyle is enforcing is better) then you're being actively hostile towards your users.

すでにCIでE129が検知されることが前提となっているテストが多く存在するため,無効化することは難しいようです.


では,いつ矛盾が発生し,いつ認知され,いつE125からif部がE129として分離したのか,時系列で追っていきます.

  • [2011-06-03] PEP8に複数行に渡る文のインデントについての記述が追加される.
  • [2012-06-01] pycodestyleにPEP8とは合い慣れないE125が実装される.
  • [2012-09-27] E125とPEP8がif文にて矛盾することがIssue#126にて指摘される.
  • [2013-03-22] 矛盾部分だけ無効化できるように.E125のif部をE129として分離.
  • [2014-04-28] PEP8にif文のインデントの書き方の具体例が加わる(上で説明した# 追加のインデントをしない).これにより明白にPEP8とE129が矛盾することに.

なるほど.

これで,E129は別に無視してもPEP8違反にはならないことがわかりました.とは言うものの,PEP8で推奨しているわけでもなく,みづらいことに変わりはないので注意が必要です.

【おまけ】VSCodeのflake8とautopep8でE129を無効化する

setting.json

    "python.linting.enabled": true,
    "python.linting.flake8Enabled": true,
    "python.linting.flake8Args": [
        "--ignore=E129",
    ],
    "python.formatting.provider": "autopep8",
    "python.formatting.autopep8Args": [
        "--ignore=E129"
    ],

参考

pycodestyle(pep8) エラーコードチートシート - Qiita

pep8 が pycodestyle に変わった話 - Qiita

E129 is wrong and misleading · Issue #474 · PyCQA/pycodestyle · GitHub

E125 overreaches pep8 · Issue #126 · PyCQA/pycodestyle · GitHub

pandasでutf-8のCSVを読み込むときにBOMの有無でどう変わるか

pandasのread_csv()はencodingオプションで,読み込ませるCSV文字コードを指定できる(デフォルトはutf-8).ここで,BOM付きutf-8CSVutf-8として読み込んでも正しく動くのかが気になった.ちなみに,BOM付きutf-8ExcelやMay 2019 Update以前のWindows10のメモ帳でCSVを作成した時に作成される.また,逆にBOMなしのutf-8をBOM付きutf-8として読み込んでも大丈夫なのか.ちょうど調べる機会があったのでまとめてみた.

疑問

  1. utf-8CSVをutf8としてread_csv()する(正常)
  2. BOM付きutf-8CSVをutf8としてread_csv()する
  3. utf-8CSVをutf_8_sigとしてread_csv()する
  4. BOM付きutf-8CSVをutf_8_sigとしてread_csv()する(正常)

1と4については問題ないはずだが,2と3は正しく読み込めるだろうか.なお,utf_8_sigはBOM付きutf-8のこと.

検証環境

検証

BOMなしutf-8とBOM付きutf-8で2つのCSVファイルを用意した.見た目上は変わらないが,バイナリ表示をすると先頭3バイトにしっかりBOM(ef bb bf)が表示されている.

$ cat data_nobom.csv
year,month,date
2020,1,1
2020,1,2
2020,1,3

$ od -A n -t x1 data_nobom.csv | xargs -L1
79 65 61 72 2c 6d 6f 6e 74 68 2c 64 61 74 65 0a
32 30 32 30 2c 31 2c 31 0a 32 30 32 30 2c 31 2c
32 0a 32 30 32 30 2c 31 2c 33 0a

$ cat data_bom.csv
year,month,date
2020,1,1
2020,1,2
2020,1,3

$ od -A n -t x1 data_bom.csv | xargs -L1
ef bb bf 79 65 61 72 2c 6d 6f 6e 74 68 2c 64 61
74 65 0a 32 30 32 30 2c 31 2c 31 0a 32 30 32 30
2c 31 2c 32 0a 32 30 32 30 2c 31 2c 33 0a

正しくread_csv()できていないと,BOM付きutf-8の場合,ファイル先頭のBOMを文字として認識してしまうはず.

In [30]: year = '\ufeffyear'

In [31]: print(year)
year

In [32]: print(year.encode())
b'\xef\xbb\xbfyear'

1. utf-8CSVをutf8としてread_csv()する

In [33]: df = pd.read_csv('data_nobom.csv', encoding='utf8')

In [35]: print(df.columns[0].encode())
b'year'

OK

2. BOM付きutf-8CSVをutf8としてread_csv()する

In [36]: df = pd.read_csv('data_bom.csv', encoding='utf8')

In [38]: print(df.columns[0].encode())
b'year'

OK

3. utf-8CSVをutf_8_sigとしてread_csv()する

In [39]: df = pd.read_csv('data_nobom.csv', encoding='utf_8_sig')

In [41]: print(df.columns[0].encode())
b'year'

OK

4. BOM付きutf-8CSVをutf_8_sigとしてread_csv()する

In [42]: df = pd.read_csv('data_bom.csv', encoding='utf_8_sig')

In [44]: print(df.columns[0].encode())
b'year'

OK

結論

結論としては,BOM付きutf-8であってもただのutf-8であっても、encodingオプションにutf_8_sigを指定してもしなくても、pandasのread_csv()は正常に動いた. (もちろん、shift-jisであればencodingにshift-jisを指定しなくては動かない)

コードで確認する

pandasのコードを確認すると,確かにbomのチェックをするコードが存在した.

        # This was the first line of the file,
        # which could contain the BOM at the
        # beginning of it.
        if self.pos == 1:
            line = self._check_for_bom(line)

https://github.com/pandas-dev/pandas/blob/master/pandas/io/parsers.py#L2844

    def _check_for_bom(self, first_row):
        """
        Checks whether the file begins with the BOM character.
        If it does, remove it. In addition, if there is quoting
        in the field subsequent to the BOM, remove it as well
        because it technically takes place at the beginning of
        the name, not the middle of it.
        """

https://github.com/pandas-dev/pandas/blob/master/pandas/io/parsers.py#L2731

encodingにかかわらず,読み込んだCSVの1行目を処理するときは必ず_check_for_bom()メソッドが呼ばれ,BOMが存在するときは取り除いている.BOMが存在しないときは何もしていない.

これで,「2. BOM付きutf-8CSVをutf8としてread_csv()する」がうまく動く理由がわかった.では,encoding='utf_8_sig'を指定したときの処理はどうなるのだろうか.「3. utf-8CSVをutf_8_sigとしてread_csv()する」場合の謎が残る.

        if encoding:
            # Encoding
            f = open(path_or_buf, mode, encoding=encoding, newline="")

https://github.com/pandas-dev/pandas/blob/master/pandas/io/common.py#L440

BOMの処理がなされたあとはpython標準のopenでcsvの処理がなされているみたい.このコードじゃないとしても,pandasのどこかでopen()かcsvモジュールが呼ばれて,引数のencodingオプションを指定しているのは容易に想像がつく.では,open()でutf-8のファイルをutf_8_sigとして読み込ませたらどうなるのか.

On encoding the utf-8-sig codec will write 0xef, 0xbb, 0xbf as the first three bytes to the file. On decoding utf-8-sig will skip those three bytes if they appear as the first three bytes in the file. In UTF-8, the use of the BOM is discouraged and should generally be avoided.

https://docs.python.org/3/library/codecs.html#standard-encodings

公式のドキュメントにちゃんと記載があった.

                if input[:3] == codecs.BOM_UTF8:
                    (output, consumed) = \
                       codecs.utf_8_decode(input[3:], errors, final)
                    return (output, consumed+3)

https://github.com/python/cpython/blob/master/Lib/encodings/utf_8_sig.py#L65

ここまで来たならコードまで確認したい.コードでも先頭3バイトにBOMがあるときだけutf_8_sigの処理をするという条件分岐が存在した.つまり,utf8なのにutf_8_sigとして読み込んでも,utf_8_sigとしての処理はされずutf8として処理されるということ.

これで,「3. utf-8CSVをutf_8_sigとしてread_csv()する」の謎も解決!pandasはかしこい!

VSCodeのLaTeX Workshopでコンパイルできない

環境

症状

.texファイルをコンパイルしようとすると,VSCodeの左下に赤く×と表示されてコンパイルされない

f:id:dorapon2000:20200107142834p:plain

LaTeX Workshopのログには実行パスが存在しないみたいなエラーが吐かれている.

[14:17:08] LaTeX build process spawned. PID: undefined.
[14:17:08] LaTeX fatal error: spawn ptex2pdf ENOENT, . PID: undefined.
[14:17:08] Does the executable exist? PATH: /usr/bin:/bin:/usr/sbin:/sbin
[14:17:10] LOG command invoked.

platexのコマンドパスを調べてみた.

$ which platex
/usr/local/bin/platex

/usr/local/binがPATHにないからエラーが出ているみたい.

$ echo $PATH | tr " " \n | grep /usr/local/bin
/usr/local/bin

でもシェル変数のPATHには/usr/local/binがある.

???

解決策

github.com

github.com

github.com

同じようなエラーで何度もissueが立てられているみたいだった.まとめると,

  • VSCodeをDockから起動したとき,シェルで設定されている$PATHをインクルードしないのは仕様
  • したがって,使用しているシェルに依存する問題ではなく,あらゆるシェルで発生する問題
  • シェルからVSCodeを起動することによって$PATHもインクルードでき,エラーはでなくなる(シェルからVSCodeを起動できるようにするには設定の必要あり)
$ cd path/to/tex/dir
$ code .

Dockから起動させても同じように/usr/local/binをパスとして含めさせたいなら,LaTeXレシピのenvプロパティで実行パスの追加ができるみたい.

手順 → Install · James-Yu/LaTeX-Workshop Wiki · GitHub

Macのスポットライト検索で計算機と定義の結果が表示されない

Macのスポットライト検索は,数式を書くと計算してくれて,英単語を書くと意味が表示されるとても便利な機能です.しかし,Catalinaにアップデートしてからか表示されなくなり原因がわからなかったのですが,直し方はわかったのでブログに残します.

環境

  • macOS Catalina version 10.15

問題

スポットライト検索の設定.

f:id:dorapon2000:20191203142419p:plain

定義と計算機だけにチェックをいれたとき,いざ検索しても何も候補が出てこない.

f:id:dorapon2000:20191203142532p:plain f:id:dorapon2000:20191203142613p:plain

直し方

定義と計算機以外の何かにチェックを入れる.

f:id:dorapon2000:20191203142813p:plain

なぜかさっきは表示されなかった計算機と定義が表示されるようになる.

f:id:dorapon2000:20191203142923p:plain f:id:dorapon2000:20191203142936p:plain

やった!🎉🎉