PyYAMLでカスタムタグを使う

調べてみたら全くといっていいほどサンプルが(英語圏にも！)なかったので簡単なのを

# きっかけ

YAMLでプログラムの挙動を定義するコードをPythonで書いていて、ちょっとYAML内にオブジェクトを生成する処理を書きたくなりました。

[PyYAMLDocumentation](https://pyyaml.org/wiki/PyYAMLDocumentation)を見る限り**タグ**というものがあって、どうもそれが目的のことをやってくれそうなのですが、どこにも利用例がなくどう使えばいいのか、どう定義すればいいのかが分かりません。さて困った。

# タグを追加する方法

で、いきなり本題です。タグを追加するには、PyYAMLモジュールのグローバル関数、`add_multi_constructor()`というものに値を渡せば良い。

```python\
  yaml.add_multi_constructor("tag:yaml.org,2002:var", var_handler)

  # ...

  def var_handler(loader, suffix, node=None):
    # 処理
```

こういうコードを書くと、YAMLファイルに以下のようなタグがあったときに反応し、`var_handler`が呼び出される。

```yaml
  textvariable: !!var:StringVar
    name: label
    default: ""
```

引数に指定した関数には三つのパラメータが引き渡され

* loader：読み込み中のYAMLファイルを処理しているローダーオブジェクトそのもの
* suffix：タグプレフィックスの後ろに続く文字列。上記YAMLコードの場合、`:StringVar`
* node：タグを発見した箇所を示すノードオブジェクト。valueにはここより下の下位層にあるノードが格納される。
  上記YAMLコードの場合、`MappingNode(value=[(ScalarNode(), ScalarNode()), (ScalarNode(), ScalarNode())])`

で、関数の戻り値としてなんらかの値をリターンすると、タグの部分が戻り値に指定した値になる。
たとえば上記例の場合、「Yes」という文字を返すと、`textvariable: Yes`と書いたのと同じようになる。

`add_multi_constructor()`関数の挙動を追ってみると、キーワード引数として定義されているLoaderを省略した場合、ここで指定した関数はLoader, FullLoader, UnsafeLoaderの三つのクラスに追加されるようです。なので、SafeLoaderを使った場合は、**ここで何をしたとしても関数は追加されません。**

## YAMLで独自に定義したタグだけを使いたい場合

YAMLを読み込むとき、独自で定義したタグのみを読み込みたい場合は、SafeLoaderを引数で指定して関数を追加すると良いです。

わたしは別の処理もあったのと、SafeLoaderを直接いじってしまうことに一抹の不安を感じたため、SafeLoaderを継承した新しいクラスを定義してしまいました。

```python
class GeneratorLoader(yaml.SafeLoader):
  def __init__(self, stream):
    super().__init__(stream)
    yaml.add_multi_constructor("tag:yaml.org,2002:var", GeneratorLoader.var_handler,
      Loader=GeneratorLoader)

  @staticmethod
  def var_handler(loader, suffix, node=None):
    pass
```

ここで作ったクラスを`yaml.load()`メソッドでLoaderとして使用すれば、タグが処理されるようになります。

### PyYAMLのグローバル関数には、Loaderのインスタンスを受け取る関数がない

なお、`yaml.load()`などPyYAMLの\_\_init\_\_.pyに定義されている関数には、Loaderのインスタンスを受け取る関数がありません(すべてクラスを受け取っています)。
このため、上記で作成したLoaderオブジェクトを、YAML読み込み後に使用したい場合は、`Loader#get_single_data()`を直接呼び出す必要があります。

```python
loader = GeneratorLoader(self.string)
struct = loader.get_single_data()
```

## その他の注意点

* タグは必ず「tag:yaml.org,2002」ではじまる必要があります。よって`add_multi_constructor()`関数の第一引数の値は必ず「tag:yaml.org,2002」からはじまります。ただし、この部分は実際にYAMLには書きません(「tag:yaml.org,2002:var」というタグを定義した場合、YAMLに書くのは「var」のみとなります)

## 説明がよくわからないからとりあえずコードを見せろ

現在作りかけですが次のモジュールで使用しています。

* [TakamiChie/TkSugar: A module that generates a structured Tk window frame from a text file\.](https://github.com/TakamiChie/TkSugar)

2020/09/21現在は`tksugar/generator.py`に当該処理があります(あとでコードを移動させるかも)。

# 参考文献

* [pyyaml/\_\_init\_\_\.py at master · yaml/pyyaml](https://github.com/yaml/pyyaml/blob/master/lib3/yaml/__init__.py#L352)
* [pyyaml/constructor\.py at master · yaml/pyyaml](https://github.com/yaml/pyyaml/blob/master/lib3/yaml/constructor.py#L166)
* [pkg\-salt/yaml\.py at b6742e03cbbfb82f4ce7db2e21a3ff31b270cdb3 · jbq/pkg\-salt](https://github.com/jbq/pkg-salt/blob/b6742e03cbbfb82f4ce7db2e21a3ff31b270cdb3/salt/utils/serializers/yaml.py)
* [CloudFormationのYAMLをPyYAMLで読み込む \| by GALACTIC1969 \| GALACTIC1969 \| Medium](https://medium.com/galactic1969/cloudformation%E3%81%AEyaml%E3%82%92pyyaml%E3%81%A7%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80-eafd5f41bf3c)
* [https://pyyaml\.org/wiki/PyYAMLDocumentation](https://pyyaml.org/wiki/PyYAMLDocumentation)