rcmdnk's blog

20221213_docstring_200_200

Pythonで関数やクラスの説明を関数名などの直下に"""で囲んだ形で書くと docstring として保存され、helpなどで説明を見る際に表示できます。

docstringに引数などの説明を与えた場合、その関数などを含むクラスを継承して、 関数に引数をいくつか足して再定義することもあるかと思います。

その際に、通常はもともとある引数の説明も全部書かないと新しいクラスでhelpしたときに表示されませんが、 ドキュメントが重複してしまうのでそこをうまく継承して重複をなくしたい、という場合にどうするか。

docstring

Pythonで、関数の最初に、

1
2
3
def func(x, y):
    """Return sum"""
    return x + y

みたいに書いておくと、

1
help(func)

とすると、

1
2
func(x, y)
    Return sum

みたいな表示をしてくれます。

これは、func__doc__という名前の値を持っていてそこに保存してあります。

1
print(func.__doc__)
1
Return sum

このdocstringがhelpで表示される際には文字列そのままではなく、インデント等をいい感じに直してから表示するようになっています。

Handling Docstring Indentation

1
2
3
def func(x, y):
    """Return sum"""
    return x + y

1
2
3
4
5
def func(x, y):
    """
    Return sum
    """
    return x + y

は、それぞれ、

1
Return sum

1
2

    Return sum

で、改行が含まれていたり先頭に空白文字が付いたり違いますが、helpでは共に同じ出力になります。

これらのdocstringは"""で始まるコメントが何らかの処理を行うより前に最初に書かれていると__doc__に収納されます。

1
2
3
4
5
6
7
8
9
10
11
def f1():
    """f1"""


def f2():
    x = 1
    """f2"""


help(f1)
help(f2)
1
2
3
4
5
...
f1()
    f1
...
f2()

また#から始まるコメント行はdocstringにはなりません。 #から始まる部分は実行時に完全に無視される形です。

1
2
3
4
5
6
def f1():
    # comment
    """f1"""


help(f1)
1
2
3
...
f1()
    f1

逆に、"""から始まる行は処理として認識されるので、

1
2
3
4
5
6
def f1():
    # comment


def f2():
    pass

と、#のコメントだけだと中身無しの関数を定義しようとすると

IndentationError: expected an indented block after function definition

のエラーが出ますが

1
2
3
4
5
6
def f1():
    """f1"""


def f2():
    pass

であればエラーになりません。

docstringのスタイル

docstringに関しては PEP 8、および PEP 257に記述がありますが、ざっくりと

  • すべてのpublicなモジュール、関数、クラス、メソッドに書く。
  • 1行目に短くサマリーを書く
    • サマリーだけの場合には"""で前後を閉じる1行で書く。
  • 詳細を追記する場合には1行空けてその後に書く。
  • 複数行にした場合は最後の行は"""だけの行で閉じるようにする。

また、PEP 8の方に、doscrtingなどは1行最大72文字まで、と書いてあります。 通常のコードは79文字まで。

短く定義されているのは、古いFortranなどでも見られる72文字制限同様、パンチカード時代の名残とか 1

また、クラス内の関数とかだと、docstringを書く位置が通常8文字スペースの後になるし、 helpの表示時にも8文字前に加わる事から80幅になる程度、という感じ。 (ただ、これだと80になるのでcodeの79の制限とはちょっと辻褄が合わない?)

この辺はcodeの文字幅も色々と議論があるところなのでわかりやすいように書けばよいかと。

PEP 257には何を書くべきか、などは書かれていますが、具体的に関数の引数や返り値の説明などを含め、どの様に書くべきか、は定義されていません。

それらに関しては、主に以下のものが使われているようです。

SphinxはPython製のドキュメント生成ツールですが、Pythonコードをドキュメントすることも出来て、その際にdosctringをいい感じにリンクとかつけて引数などを書いてくれます。 そのため、ちょっとコードチックな書き方になっています。 なお、SphinxではNumPy/Google Styleのdocstringでも拡張機能を使うことでドキュメント化することができるとのこと。

NumPyやGoogleのものは同じような感じで、引数の書き方などが少し違う感じです。

引数や返り値に関する記述の仕方としては

Numpy:

1
2
3
4
5
6
7
8
9
10
11
12
Parameters
----------
x : int
    `x` value
y : int
    `y` value


Returns
-------
sum : int
    Sum of x and y

Google:

1
2
3
4
5
6
7
8
9
10
11
Args
----
  x :
      `x` value
  y :
      `y` value


Returns
-------
  Sum of x and y

引数を表すセクションが、NumPyはParameter、GooleがArgsとなっているのが大きな違い。

細かい違いとして、Googleの方は引数などがインデントされて書かれているところや、:の後に1行にまとめても良い、となっているところ。 また、NumPyは引数の名前の後に型を書きますが、Googleの方は型に関しては

The description should include required type(s) if the code does not contain a corresponding type annotation

という感じで説明文中に書くように書かれています。

コードの動作自体に影響があるわけではないのでこの辺りは好きなものを。

継承先のクラスでのdocstring

クラス内の関数のdocstringは、継承先のクラスで再定義された場合、docstringが書かれていれば上書きされます。 再定義されなかったり、再定義されてもdocstringが書かれてなければ元のクラスで定義されたものがそのまま使われます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class A:
    def f1():
        """A.f1"""
        print('f1')
        return

    def f2():
        """A.f2"""
        print('f2')
        return

    def f3():
        """A.f3"""
        print('f3')
        return


class B(A):
    def f2():
        print('f2')
        return

    def f3():
        """B.f3"""
        print('f3')
        return


help(B.f1)
help(B.f2)
help(B.f3)
1
2
3
4
5
6
7
8
f1()
    A.f1

f2()
    A.f2

f3()
    B.f3

こんな感じ。

クラス自体にもdocstringがつけられますが、こちらは継承先で書かれなければ何も出ません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
    """Class A"""


class B(A):
    pass


class C(A):
    """Class C"""
    pass


help(A)
help(B)
help(C)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
class A(builtins.object)
 |  Class A
 |
 |  Data descriptors defined here:
...
class B(A)
 |  Method resolution order:
 |      B
 |      A
 |      builtins.object
 |
 |  Data descriptors inherited from A:
...
class C(A)
 |  Class C
 |
 |  Method resolution order:
 |      C
 |      A
 |      builtins.object
 |
 |  Data descriptors inherited from A:

ParametersやReturnsを引き継ぎたい

クラスを継承した際、クラス内の関数を再定義する場合でも基本的には 引数などは同じもので、内部処理が違うといったことが多いかと思います。

その場合、docstringの最初の説明部分やReturnsの説明部分は変更するために書く必要がありますが、 Parameters(Args)に関しては、親のクラスと全く同じものであることもあります。

それを重複して書くのは無駄なのでできれば変数みたいにして引き継いだりしたいところです。

あらかじめ変数を定義して中で使う方法

よく使う変数など、複数の関数で同じ説明を書くこともあるかと思います。 これはクラス継承などなくとも起こることです。

そのようなときはあらかじめそれを変数に入れておいて使えれば便利です。

ただし、docstringではformatやfstringのような文字列の代入は出来ません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name1 = "f1"


def f1():
    """This function is {name}""".format(name=name1)


name2 = "f2"


def f2():
    f"""This function is {name2}"""


help(f1)
help(f2)
1
2
3
f1()

f2()

両方ともこれらの行は何らかの処理とみなされるだけで__doc__には入りません。

ここで無理やり使うためには__doc__を後から直接変更します。

1
2
3
4
5
6
7
8
9
10
11
name1 = "f1"


def f1():
    """This function is {name}"""


f1.__doc__ = f1.__doc__.format(name=name1)


help(f1)
1
2
f1()
    This function is f1

fstringはそのままだと無理なので、evalとかを使えば出来ないことはないですが 2 この場合はformatを使う方が無難です。

Classのdocstringに関しては、__doc__に代入する形で直接formatやfstringで定義することが出来ます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name1 = "C1"


class C1:
    __doc__ = """Class {name}""".format(name=name1)


name2 = "C2"


class C2:
    __doc__ = f"""Class {name2}"""


help(C1)
help(C2)
1
2
3
4
5
6
7
8
9
10
11
...
class C1(builtins.object)
 |  Class C1
 |
 |  Data descriptors defined here:
...
class C2(builtins.object)
 |  Class C2
 |
 |  Data descriptors defined here:
...

こちらはfstringでも可能です。

同じように関数で直接__doc__を指定してもうまくいきません。

このように変数が使えるなら、必要な変数をまとめて定義しておいて、継承先のクラスでも それを使って書けば無駄が省けます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
parameters = """x : int
        first argument
    y : int
        second argument"""
"""

class A:
    __doc__ = """A class

    Parameters
    ----------
    {parameters}
    """.format(parameters=parameters)

    def __init__(self, x, y):
        pass


class B:
    __doc__ = f"""B class

    Parameters
    ----------
    {parameters}
    z : int
        third argument
    """

    def __init__(self, x, y, z):
        pass


help(A)
help(B)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
class A(builtins.object)
 |  A(x, y)
 |
 |  A class
 |
 |  Parameters
 |  ----------
 |  x : int
 |      first argument
 |  y : int
 |      second argument
 |
 |  Methods defined here:
...
class B(builtins.object)
 |  B(x, y, z)
 |
 |  B class
 |
 |  Parameters
 |  ----------
 |  x : int
 |      first argument
 |  y : int
 |      second argument
 |  z : int
 |      third argument
 |
 |  Methods defined here:

インデントをきちんと考慮してあげないとずれてしまうので、最初の変数を作るときに 少し気をつける必要があります。

あらかじめ変数を定義して中で使う方法 with decorator

上と同じことをデコレーターを使って実現することも出来ます。

title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
docstring_dict = {
  "params": """x : int
        first argument
    y : int
        second argument""",
  "z": """z: int
        third argument""",
}
docstring_dict_class_func = {k: v.replace('\n', '\n    ') for k, v in docstring_dict.items()}
print(docstring_dict_class_func)


def parse_docstring(obj):
    obj.__doc__ = obj.__doc__.format(**docstring_dict)
    return obj


def parse_docstring_class_func(obj):
    obj.__doc__ = obj.__doc__.format(**docstring_dict_class_func)
    return obj

こんな関数を用意しておいて、

title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@parse_docstring
def f1(x, y, z):
    """Function f1

    Parameters
    ----------
    {params}
    {z}
    """


@parse_docstring
class A:
    """Class A

    Parameters
    ----------
    {params}
    {z}
    """

    @parse_docstring_class_func
    def f2(x, y, z):
        """Function f2

        Parameters
        ----------
        {params}
        {z}
        """
help(f1)
help(A)
help(A.f2)

とすれば

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
f1(x, y, z)
    Function f1

    Parameters
    ----------
    x : int
        first argument
    y : int
        second argument
    z: int
        third argument

class A(builtins.object)
 |  Class A
 |
 |  Parameters
 |  ----------
 |  x : int
 |      first argument
 |  y : int
 |      second argument
 |  z: int
 |      third argument
 |
 |  Methods defined here:
 |
 |  f2(x, y, z)
 |      Function f2
 |
 |      Parameters
 |      ----------
 |      x : int
 |          first argument
 |      y : int
 |          second argument
 |      z: int
 |          third argument
 |
 |  ----------------------------------------------------------------------
...

こんな感じになります。

クラス内関数ではインデントの量を直したものを別途用意している部分は もうちょっとスマートに出来ると思いますが、とりあえずはこんな感じで必要な事が出来るかと思います。

上の例ではグローバル変数を直接デコレーターの中で使ってますが、 引数を持つデコレーターにして、必要な変数だけ入力するようにしてあげることも出来ます 3

title
1
2
3
4
5
6
7
8
9
10
11
12
13
params = """x : int
        first argument
    y : int
        second argument"""
z = """z: int
        third argument"""


def parse_docstring(**kw):
    def dec(obj):
        obj.__doc__ = obj.__doc__.format(**kw)
        return obj
    return dec

みたいにして、

title
1
2
3
4
5
6
7
8
9
@parse_docstring(params=params, z=z)
def f1(x, y, z):
    """Function f1

    Parameters
    ----------
    {params}
    {z}
    """

こんな感じ。

__init_subclass__を利用する

Python 3.6から導入された__init_subclass__は、 クラスが継承された時に実行される関数です。

title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
    def __init_subclass__(cls, **kw):
        print("A.__init_subclass__", cls, kw)
        super().__init_subclass__(**kw)


class B(A):
    pass


class C(A):
    pass


class D(B):
    pass
1
2
3
A.__init_subclass__ <class '__main__.B'> {}
A.__init_subclass__ <class '__main__.C'> {}
A.__init_subclass__ <class '__main__.D'> {}

これを利用して、関数を再定義した際、 親クラスの関数などの__doc__を引き継ぐようにすることができます。

1
2
3
4
5
6
7
8
9
10
def __init_subclass__(cls, **kwargs):
    super().__init_subclass__(**kwargs)
    parent_method_docstr = {}
    for i, v in ParentClass.__dict__.items():
        if v and callable(v) and v.__doc__ is not None:
            parent_method_docstr[i] = v.__doc__

    for i, v in cls.__dict__.items():
        if v and callable(v) and v.__doc__ is None and i in parent_method_docstr:
            v.__doc__ = parent_method_docstr[i]

Inherit docstrings in Python class inheritance - Stack Overflow)

この例は再定義した際にdocstringを書かなかった場合、親クラスの同じ関数のdocstringを そのまま引き継ぐようにしています。

Parametersの部分だけ引き継ぎたかったりする場合には、 新しい関数にもdocstringを書いて、それを良い感じで元のdocstringと合わせる様な 処理を入れる必要があります。

また、同じ様なことをメタクラスを使って__new__関数の中で色々とやることも出来ます。 上のような簡単な例であれば__init_subclass__の方がすっきりと出来ますが、 複雑なことをやろうと思ったらメタクラスを定義したほうがきれいに書けることもあるかと思います。

既存のライブラリを利用する

PyPiで公開されているライブラリで使えそうなものは以下のもの

docstring-inheritance

親クラスにメタクラスを導入し、クラスのdocstringやクラス内の関数のdocstringで、ParametersやReturnsなどを 引き継げる様にしてくれるライブラリ。

基本的な考え方としては上の__init_subclass__と同じですが、必要なものを必要なだけ引き継いで 整形してくれるので便利です。

関数にParametersの欄がある場合、引き継ぎ先のクラスではParametersを書く際に 親クラスで定義されたものは再度書く必要がなく、新たなものだけを書けば良いようになっています。

また、引数の名前を変えたり削除した場合にはdocstringからも削除されるようになっています。

NumpyDocstringInheritanceMetaGoogleDocstringInheritanceMetaという2つのメタクラスが用意されていて NumPyとGoogleのdocstringスタイルにそれぞれ対応しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from docstring_inheritance import NumpyDocstringInheritanceMeta


class Parent(metaclass=NumpyDocstringInheritanceMeta):
    """Parent class

    Attributes
    ----------
    a: int
        Description for a.
    b: int
        Description for a.
    """

    def meth(self, x, y=None):
        """Parent method.

        Parameters
        ----------
        x:
           Description for x.
        y:
           Description for y.


        Returns
        -------
        int
           Description for return value.


        Notes
        -----
        Parent notes.
        """


class Child(Parent):
    """Child class

    Attributes
    ----------
    c: int
        Description for c.
    """

    def meth(self, x, z):
        """Child method.

        Parameters
        ----------
        z:
           Description for z.

        """


help(Child)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Child(Parent)
 |  Child class
 |
 |  Attributes
 |  ----------
 |  a: int
 |      Description for a.
 |  b: int
 |      Description for a.
 |  c: int
 |      Description for c.
 |
 |  Method resolution order:
 |      Child
 |      Parent
 |      builtins.object
 |
 |  Methods defined here:
 |
 |  meth(self, x, z)
 |      Child method.
 |
 |      Parameters
 |      ----------
 |      x:
 |         Description for x.
 |      z:
 |         Description for z.
 |
 |      Returns
 |      -------
 |      int
 |         Description for return value.
 |
 |      Notes
 |      -----
 |      Parent notes.

こんな感じで、

  • クラスのAttributesにParentで書かれたものが追加されている。
  • methのParametersは、yが削除されているのでdocstringからも消えている。代わりにzが追加され、新たに書いたものが加えられている。もともとあったxも残っている。
  • その他、書かれなかった部分に関してはParentのものがそのまま残っている。

という感じで、Parametersとかを追加する際に追加分だけ書けば元の文章とマージして表示してくれます。

クラスのdocstringも関数のdocstringについてもやってくれます。

ただ、使ってて1つ上手くいかないところがあって、クラスのdocstringでParametersを使うと引き継ぎが出来ません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from docstring_inheritance import NumpyDocstringInheritanceMeta


class Parent(metaclass=NumpyDocstringInheritanceMeta):
    """Parent class

    Parameters
    ----------
    a: int
        Description for a.
    b: int
        Description for a.
    """


class Child(Parent):
    """Child class

    Parameters
    ----------
    c: int
        Description for c.
    """


help(Child)

だと

1
2
3
4
5
6
7
8
class Child(Parent)
 |  Child class
 |
 |  Parameters
 |  ----------
 |
 |  Method resolution order:
...

こんな感じで新たに書いたものも含めて全部消えてしまいます。

Parametersと言っているのは__init__の引数にあたるものですが、 上で__init__を書いても変わりません。

これはIssueにもなってました。

Inheritance in Constructor Does Not Work · Issue #1 · AntoineD/docstring-inheritance

できそうではあるけどちょっと色々と面倒なので時間をくれ、とのこと。

これに対して、とりあえずの対処法として、

https://github.com/AntoineD/docstring-inheritance/blob/bc5f94300e3661d5ead36730738e9621639a2f97/src/docstring_inheritance/processors/numpy.py#L48

にある、Parameters_ARGS_SECTION_ITEMS_NAMESの欄から_SECTION_ITEM_NAMESの欄の方に持っていけばとりあえずクラスのdocstringでもParametersを引き継いでくれるようになります。

以下の様なメタクラスを新たに定義してやってNumpyModDocstringInheritanceMetaNumpyDocstringInheritanceMetaの代わりにつかってやればOK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from docstring_inheritance import DocstringInheritanceMeta
from docstring_inheritance.processors.numpy import NumpyDocstringProcessor


class NumpyModDocstringProcessor(NumpyDocstringProcessor):
    _ARGS_SECTION_ITEMS_NAMES = {
        "Other Parameters",
    }

    _SECTION_ITEMS_NAMES = _ARGS_SECTION_ITEMS_NAMES | {
        "Parameters",
        "Attributes",
        "Methods",
    }


NumpyModDocstringInheritanceMeta = DocstringInheritanceMeta(
    NumpyModDocstringProcessor()
)

ただし、これをやると関数の方も含め、Parametersのところで、変数が削除された場合にも そのまま表示されるようになってしまいます。

上の_ARGS_SECTION_ITEMS_NAMESのグループに入っていると 変数の削除とかのチェックが行われるようです。

Attributesの方はもともと継承先のクラスで減ることは想定されてないので もともと下にあるAttributesは実際追加しかされません。

で、それがクラスのdocstringと関数のところで何が違うのか、というところまでちゃんと理解しきれてないので 一旦上の状態を考えてます。

関数の引数も基本的には追加することはあれど名前を変えたり削除する必要は ないので大丈夫かな、とは。

少し気になった点として、mypyを使って型チェックを行うと、

error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases [misc]

みたいなエラーが継承先のクラスの定義の部分で出てしまいます。 実際にはエラーに出ているような複数のメタクラスが定義されていてconflictしている、といった状態ではないはずなのですが。

しかも二回目以降は

AssertionError: Should never get here in normal mode, got Var:apaf.docstring.NumpyModDocstringInheritanceMeta instead of TypeInfo

といったエラーが起きて解析自体ができなくなる状態。

この状態になったら一旦.mypy_cacheを消してやると治ります。

ただ、上のエラーがまた出てしまうので、classの定義の部分で # type: ignore[misc]を入れて無視するようにするしかありません。

これに関してはおそらくmypy側の問題で下のIssueに関係がありそう。

mypy Type error for a class with an imported metaclass · Issue #9185 · python/mypy

docrep

こちらはデコレーターを使って関数生成時にその関数のdocstringから情報を抽出して 別の関数で再利用する、といったもの。

なのでクラスの継承とか関係なく使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import docrep

docstrings = docrep.DocstringProcessor()

@docstrings.get_sections(base='do_something', sections=["Parameters", "Returns"])
@docstrings.dedent
def f1(a, b):
    """f1

    Parameters
    ----------
    a: int
        The first number
    b: int
        The second number

    Returns
    -------
    int
        `a` + `b`
    """


@docstrings.dedent
def do_more(a, c):
    """f2

    Parameters
    ----------
    %(do_something.parameters)s
    c: int
        The third number


    Returns
    -------
    %(do_something.returns)s
    """


help(do_more)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
do_more(a, c)
    f2

    Parameters
    ----------
    a: int
        The first number
    b: int
        The second number
    c: int
        The third number


    Returns
    -------
    int
        `a` + `b`

@docstrings.get_sectionsをかけることでdocstringからsectionごとの情報を取得します。 取得されるsectionはデフォルトではParametersOther Parametersなのでここでは ParametersReturnsに変更して取得。

取得すると、@docstrings.dedentのデコレーターを次に与える時に 上の様に変数としてsectionごと与える事が出来ます。

この場合は引数の削除などは見てないので上のように削除されてもそのまま表示されます。

クラスのdocstringに使うには少し工夫が必要で、__doc__を直接つくるような形で 取り込む必要があります。

Reuse docstring from Class doc · Issue #2 · Chilipp/docrep

また、クラス内の関数に関しても必要な部分全てにdedentなどのデコレーターを与えないといけません。

逆に必要な部分にだけ必要なパラメーターを与えるような調整はしやすい面はあるので、 使い方によっては使いやすいかもしれません。

Ref:

custom_inherit

こちらはクラスに対してメタクラスを与えるかデコレーターをつけて 継承先のクラスでParametersなどを引き継げるようにしたもの。

こちらは関数のdocstringだけでクラスのdocstringは引き継がれません。

その他

他にもdocstring周りではこんな議論があったらしい

Sponsored Links
  1. python - How can I use f-string with a variable, not with a string literal? - Stack Overflow

  2. この最後にある

    I’m using Python 3.8 Simple string formatting worked for me. “"”This is {}””“.format(“StackOverflow”)

    はなぜか2票入ってますが、3.8で試してもダメですし、3.10とかでもやはりダメでした。

Sponsored Links

« Homebrewで古いバージョンを使う([email protected]が動かない) pre-commitでShellCheckを使う »

}