Python ASTパーシングとカスタムリント
Table of Contents:
- はじめに
- 目次
- カスタムチェックの作成方法
- flake8とは?
- カスタムプラグインの作成
- プラグインの設定
- エラーの出力方法
- チェックの追加方法
- オプションの追加方法
- カスタムルールのパッケージ化方法
目次
1. はじめに
2. カスタムチェックの作成方法
3. flake8とは?
4. カスタムプラグインの作成
5. プラグインの設定
6. エラーの出力方法
7. チェックの追加方法
8. オプションの追加方法
9. カスタムルールのパッケージ化方法
はじめに
こんにちは。私の名前はJames Murphyです。この記事では、Pythonコード内でPythonコードをプログラム的にトラバースする方法について学びます。そして、その能力を使ってカスタムチェックを作成し、一般的なエラーを防ぎ、コードの品質を高めます。具体的には、flake8用のカスタムプラグインを作成します。flake8は、ソースコードを実行し、問題があるかどうかを通知するツールです。この例では、cool_moduleには問題が見つかりませんでした。ただし、関数に未使用の変数を追加し、再度linterを実行すると、エラーが表示されます。ローカル変数xが割り当てられましたが、使用されません。 linterは、スタイルやコードスタイルの問題についても警告できます。例えば、この場合、ファイルの末尾に改行がないことを教えてくれます。flake8のようなlinterにはすでにたくさんの優れたルールが組み込まれていますが、好みに合わないものは無効にすることもできます。しかし、独自のルールを追加したい場合はどうすればよいでしょうか?例えば、関数内でのローカルインポートを禁止したいとしましょう。関数内にインポート文があること自体には問題ありませんが、これらの関数レベルのインポートは、循環依存性の問題を回避するための仕掛けになることがあります。基本的には、ローカルインポートは、より深いアーキテクチャの問題を隠している可能性があるため、それを防止したいのです。しかし、次に大きな問題に直面します。モジュール内のすべてのローカルインポートを見つけるように求められた場合、どのようにしてそれを行うことができるでしょうか?おそらく正規表現を使って、ローカルインポートの形式に一致するようにするでしょう。ただし、この単純なタスクでも、正規表現を使って行うのは非常に困難です。正規表現を使用しようとすると、テキストはPythonコードを考える自然な方法ではありません。私たちはこれらのコンセプトを、それらを構成する文字のリテラルとしてではなく、より抽象的なレベルで、クラス、関数、forループなどとして考えます。幸いなことに、Python自体もこのように考えており、このデータを私たちに公開することができます。これを実現するためにastモジュールを使用します。cool_moduleにastを実行すると、以下のように表示されます。モジュール、関数定義、引数などの情報が表示されます。これが抽象構文木と呼ばれるものです。これは、Pythonソースコードを分析するために必要なデータ構造です。ソースファイルにastを実行すると、抽象構文木が表示されます。また、自分自身のコードからastをインポートし、実行時にその木にアクセスすることもできます。この例では、ソースモジュールからコードを読み取り、ast.parse()関数に渡して、木のオンメモリ表現を取得しています。得られたノードを表示すると、トップレベルのモジュールオブジェクトが表示されます。これらのastオブジェクトはすべて、アンダースコアフィールド属性を持っています。したがって、それらの内部の内容を確認することができます。astのドキュメントにも記載されています。モジュールにはbodyフィールドがあります。それを表示すると、bodyがステートメントのリストであることがわかります。ここでは、関数定義だけが含まれています。さらに掘り下げて、関数定義の名前、引数、本体などを取得できます。astのこのプリントアウトに表示されているすべての要素は、実際に実行時にアクセスできます。トップレベルのノードオブジェクトから始めて、node.body[0].nameのようにして、名前を表示することもできます。このような構造があるおかげで、文字のリテラルではなく、Pythonの構築物を使用して、Pythonコードをトラバースできるようになりました。astはまた、NodeVisitorクラスを提供しています。このクラスを派生させることで、木全体を簡単にトラバースし、すべてのノードを訪れることができます。ここでは、ルートノードに対してvisitを呼び出し、visitはノードを表示し、このgeneric_visitを呼び出してこのノードのすべての子を訪れるだけです。出力は次のようになります。モジュールから関数定義に移動します。基本的に、木のすべてのノードに対して単一の呼び出しがあります。ただし、通常、コードをリントし、特定のルールを適用する場合、それはすべての種類のオブジェクトに適用されるわけではありません。モジュールやクラス、関数には適用されません。たとえば、forループに関する一般的なエラーに関するルールがある場合、すべてを訪問するのではなく、visit_Forというように指定することができます。それを行うと、2つの呼び出しのみが表示されます。なぜなら、cool_moduleには2つのforループしかないからです。これらの名前の詳細はastのドキュメントで確認できます。基本的に、Pythonの可能性のあるすべての構造に対して、1つのルールがあります。では、目標に戻りましょう。関数内のインポートを検出したら警告するという目標に戻りましょう。もし私のコードをflake8に呼び出してもらえれば、すべてのローカルインポートを見つけることができると自信があります。では、flake8にコードを呼び出すにはどうすればよいですか?実際には、非常にシンプルです。まず、プラグインのクラスを作成し、名前とバージョンを指定します。この名前とバージョンは必須であり、ヘルプテキストに表示されます。次に、astを引数に取るinit関数を作成します。flake8はシグネチャを調べて、astを渡すことを知ります。flake8はプラグインを構築し、その後runを呼び出します。そして、runは見つかったエラーをyieldする必要があります。ここでは、単に何もyieldしていません。次に、flake8の構成ファイルである.flake8を作成して、プラグインの場所を指定します。これは、標準のiniファイル形式を使用します。local-pluginsのブロックを作成し、拡張機能を作成します。このように複数の拡張機能について指定することもできますが、今は1つだけにしましょう。左側にはエラーに使用するコード接頭辞を指定します。この例では、mcoding 1を使用しています。つまり、すべてのエラーがMCOD1で始まることを意味します。右側には、モジュールへのドット区切りのパスを指定します。この例では、モジュールは同じディレクトリにあるため、.を指定します。その後にコロンとクラスの名前が続きます。モジュールが現在の仮想環境のインストールパッケージではない場合、パスをCurrent directoryに設定して、flake8がプラグインを見つけられないと警告が表示されます。cool_module上でflake8を実行すると、プラグインから出力されたテキストが表示されます。この時点では、プラグインは実行されても実際には何も行っていません。エラーをyieldする方法はどのようなものでしょうか?flake8はエラーを4つのタプルとしてyieldすることを期待しており、それには行番号、列オフセット、エラーメッセージ、およびクラスが含まれています。行番号、列オフセット、メッセージの説明は比較的明確ですが、クラスとは何でしょうか?実際には何も行われないが、そこにある必要があり、一般的にはエラーを生成したクラスを返します。動作を確認するため、常に1行目のオフセット0でエラーをyieldしましょう。cool_module上でflake8を実行すると、カスタムエラーが表示されます。エラーメッセージは、構成ファイルで指定した接頭辞で始まっている必要があることに注意してください。指定した接頭辞をMCOD2に変更して、再度実行すると、エラーは表示されなくなります。元に戻します。ただし、これには注意が必要です。目標である関数内のインポートを見つけることに成功しましたが、それだけではありません。リントルールを追加したい場合は、複数のNodeVisitorを作成したくありません。大規模なコードベースの場合、完全な抽象構文木をトラバースするのは非常にコストがかかります。実際、私はそれを少しリファクタリングして、複数のチェックを追加する際に同じツリーを複数回トラバースすることなく、より簡単にするでしょう。したがって、ここで行ったことは、ツリーをチェックし、問題があるかどうかを確認するために実際にチェックを実行する部分を抽出し、それをcheckという関数にまとめました。その関数をクラスに包み込み、メッセージをクラス属性として設定しました。新しいチェックを追加するために書く場合は、単に新しいクラスを作成し、メッセージとチェック関数を指定します。チェック関数は、追加するエラーのリストを渡すようにしておきます。NodeVisitorでのチェック関数の呼び出しによって、実際に実行するチェッククラスのチェック関数が呼び出されます。プラグインにオプションを追加する場合(strictフラグなど)、それも可能です。プラグイン内で、option_managerを引数に取る@staticmethod add_optionsを作成します。option_managerは、次のようにしてインポートできます。argparseを使用したことがある場合は、option_managerは非常に馴染みのあるものになるはずです。argparseをラッピングしたものであり、オプションの型や変数の名前などを指定します。これはヘルプテキストに表示されます。デフォルト値も指定できます。これにより、ユーザーが設定ファイルから値を設定し、ヘルプテキストを指定できます。この段階では、--helpオプションでflake8を実行すると、プラグインのドキュメンテーションが表示されます。--some-fancy-variable、名前のn、および説明が表示されています。指定されたオプションの値を取得するためには、parse_optionsという関数を追加する必要があります。これもまた@staticmethodであり、実際のargparse.Namespaceをパラメータとして受け取ります。設定した変数はすべてoptions変数の属性として使用できるようになります。たとえば、オプションに基づいてstrictモードを有効にするかどうかなど、オプションに基づいて何かを設定する場合は、ここにフラグを設定します。また、これはrunメソッドが呼び出される前に呼び出されるので、すべてのオプションと設定された値が実行前に利用できるようになります。コードの確認が必要な場合は、宿題を提供します。まず、evalの使用を警告するプラグインを作成してください。トリッキーケースについては心配しないでください。リテラルevalのみをチェックしてください。次に、新しいメタクラスの作成を禁止してください。最後に、複数の代入で同じ名前の複数の使用を禁止するプラグインを作成してください。これは、混乱の余地のある人気のある例です。このような使用は許可されないはずです。名前aはここで複数回使用されており、許可されていません。複数の代入についての動作説明をご覧になりたい場合は、私のビデオをご覧ください。そして最後に、本格的に独自のリントルールを書きたい場合は、自分のリンティングコードをパッケージ化することを強くお勧めします。現在、すべてのこのコードは、すべてのファイルが同じディレクトリにあることを前提としています。これは非常に不安定です。実際のプロジェクトでは、カスタムルールをパッケージ化して、単にpip installできるようにすることをお勧めします。プロジェクトのパッケージ化方法については、自動テストに関するビデオをご覧ください。いつものように、ご視聴ありがとうございます。私のパトロンと寄付者にも感謝します。お会いできることを楽しみにしています。
カスタムチェックの作成方法
Pythonコード内でPythonコードをプログラム的にトラバースする方法について学ぶ前に、カスタムチェックの作成方法について説明します。カスタムチェックを作成する際には、flake8というツールを使います。flake8は、ソースコードを実行し、問題があるかどうかを通知するツールです。カスタムチェックを作成するには、以下の手順を実行します。
-
カスタムプラグインのクラスを作成します。このクラスには名前とバージョンを指定する必要があります。
class CustomPlugin:
name = "custom_plugin"
version = "1.0.0"
-
カスタムプラグインに__init__
関数を追加します。この関数は、flake8がast(抽象構文木)を渡すための引数を受け取ります。
class CustomPlugin:
name = "custom_plugin"
version = "1.0.0"
def __init__(self, tree):
self.tree = tree
-
カスタムプラグインにrun
メソッドを追加します。このメソッドは、チェックしているエラーをyieldする必要があります。
class CustomPlugin:
name = "custom_plugin"
version = "1.0.0"
def __init__(self, tree):
self.tree = tree
def run(self):
# エラーチェックを実行しエラーをyieldする
yield
-
カスタムプラグインの設定ファイルを作成します。設定ファイルは、プラグインの場所をflake8に教えるために使用されます。
[flake8]
local-plugins = custom_plugin
以上の手順で、カスタムチェックの基本的な構造を作成することができます。エラーチェックの詳細や追加の設定方法については、後ほど説明します。では、実際の例として、関数内のローカルインポートを禁止するカスタムチェックを作成してみましょう。
flake8とは?
flake8は、Pythonコードの品質をチェックするためのツールです。ソースコードを実行し、コードのスタイルやエラーに関する問題を検出します。また、カスタムプラグインを使用することで、独自のチェックも追加することができます。また、flake8は以下のような機能も提供しています。
- スタイルガイドに従ったコードのチェック
- コードカバレッジの計算
- エラーの表示とフォーマットのカスタマイズ
- さまざまな設定オプションの提供
これにより、コードの品質を向上させるための強力なツールになっています。
カスタムプラグインの作成
カスタムプラグインを作成する際には、flake8のプラグインアーキテクチャを理解する必要があります。プラグインは、特定のルールを適用するためのクラスです。以下の手順を実行して、カスタムプラグインを作成します。
-
カスタムプラグインのクラスを作成します。このクラスには名前とバージョンを指定する必要があります。
class CustomPlugin:
name = "custom_plugin"
version = "1.0.0"
-
カスタムプラグインに__init__
関数を追加します。この関数は、flake8がast(抽象構文木)を渡すための引数を受け取ります。
class CustomPlugin:
name = "custom_plugin"
version = "1.0.0"
def __init__(self, tree):
self.tree = tree
-
カスタムプラグインにrun
メソッドを追加します。このメソッドは、チェックしているエラーをyieldする必要があります。
class CustomPlugin:
name = "custom_plugin"
version = "1.0.0"
def __init__(self, tree):
self.tree = tree
def run(self):
# エラーチェックを実行しエラーをyieldする
yield
これで、カスタムプラグインの基本的な構造が完成しました。次に、エラーチェックの具体的な方法について説明します。
プラグインの設定
カスタムプラグインの設定方法について説明します。プラグインの設定は、flake8の設定ファイルで行います。設定ファイルは、モジュール内のファイルであり、以下のような形式で書かれています。
[flake8]
local-plugins = custom_plugin
上記の設定では、custom_plugin
という名前のプラグインを使用するように指定しています。この設定ファイルは、flake8の実行時に自動的に参照されます。
エラーの出力方法
カスタムプラグインを作成する際には、エラーの出力方法も考慮する必要があります。flake8はエラーを4つのタプルとしてyieldすることを期待しており、それには以下の情報が含まれています。
- 行番号
- 列オフセット
- エラーメッセージ
- エラーのクラス
これらの情報を使用して、エラーチェックを実行し、エラーを出力します。以下に例を示します。
# エラーチェックを実行しエラーをyieldする
def run(self):
errors = []
# エラーチェック処理
if error:
errors.append((line_number, column_offset, "エラーメッセージ", CustomError))
yield from errors
エラーチェックに使用する特定の条件に応じてエラーをyieldすることができます。
チェックの追加方法
カスタムプラグインに追加のチェックを実装することができます。これにより、異なる種類のエラーを検出することができます。以下の手順に従って、チェックを追加します。
-
チェックを実装するクラスを作成します。このクラスにはチェックの名前とバージョンを指定します。
class CustomCheck:
name = "custom_check"
version = "1.0.0"
-
チェックを実行するための関数を追加します。この関数には、チェック処理が含まれます。
def custom_check(tree):
errors = []
# チェック処理
if error:
errors.append((line_number, column_offset, "エラーメッセージ", CustomError))
return errors
-
カスタムプラグインのrun
メソッド内で、チェック関数を呼び出します。
class CustomPlugin:
name = "custom_plugin"
version = "1.0.0"
def __init__(self, tree):
self.tree = tree
def run(self):
# チェック関数を呼び出してエラーを取得
errors = custom_check(self.tree)
yield from errors
これで、カスタムプラグインに新しいチェックを追加することができます。
オプションの追加方法
カスタムプラグインにオプションを追加することができます。オプションを使用すると、プラグインの動作をカスタマイズすることができます。以下の手順に従って、オプションを追加します。
-
オプションを追加するためのadd_options
メソッドを追加します。このメソッドは、option_managerというオプションマネージャーを引数に取ります。
@staticmethod
def add_options(option_manager):
option_manager.add_option(
"--some-fancy-variable",
type="int",
dest="some_variable",
default=10,
help="Some fancy variable"
)
-
オプションの値を取得するために、parse_options
メソッドを追加します。このメソッドは、options
というパラメーターを受け取ります。
@staticmethod
def parse_options(options):
some_variable = options.some_variable
# オプションの値を使用して何かを行う
これで、カスタムプラグインにオプションを追加することができます。その後、プラグインの振る舞いをオプションに基づいてカスタマイズすることができます。
カスタムルールのパッケージング方法
独自のリントルールを書く場合は、カスタムルールをパッケージ化することをお勧めします。これにより、カスタムルールを簡単に共有したり、他のプロジェクトで再利用したりすることができます。以下の手順に従って、カスタムルールをパッケージ化します。
-
カスタムルールをPythonモジュールとして作成します。
-
setup.py
ファイルを作成し、パッケージの情報を設定します。
from setuptools import setup, find_packages
setup(
name="custom_rules",
version="1.0.0",
packages=find_packages(),
install_requires=[
"flake8"
],
)
-
setup.py
ファイルを使ってパッケージをインストールします。
$ python setup.py install
-
パッケージが正しくインストールされたことを確認します。
$ flake8 --version
これで、カスタムルールをパッケージ化することができました。他のプロジェクトでカスタムルールを使用するには、単にpip install custom_rules
と実行すればよいです。
まとめ
本記事では、Pythonコードの品質チェックにカスタムプラグインを作成する方法について説明しました。カスタムプラグインを使用することで、ソースコード内の問題を自動的に検出し、品質を向上させることができます。また、カスタムプラグインを作成する際には、エラーの検出方法や出力方法、さらにはオプションの追加方法まで学びました。カスタムルールのパッケージング方法についても触れました。これで、自分自身のリントルールを作成し、Pythonコードの品質を向上させる準備ができました。是非、カスタムプラグインを作成してみてください。
FAQ:
Q: カスタムプラグインを作成する際の必要なスキルはありますか?
A: カスタムプラグインを作成するためには、Pythonの基本的な知識が必要です。また、flake8やastモジュールについての理解も必要です。しかし、これらの知識は容易に学ぶことができるため、初心者でも取り組むことができます。
Q: カスタムプラグインを作成する利点はありますか?
A: カスタムプラグインを作成することにより、自分自身のリントルールを追加できます。これにより、特定のコーディングスタイルやエラーパターンに対して自動的にチェックを行うことができます。コードの品質向上に役立ちます。
資料: