matplotlib

Форк
0
/
check_typehints.py 
319 строк · 10.8 Кб
1
#!/usr/bin/env python
2
"""
3
Perform AST checks to validate consistency of type hints with implementation.
4

5
NOTE: in most cases ``stubtest`` (distributed as part of ``mypy``)  should be preferred
6

7
This script was written before the configuration of ``stubtest`` was well understood.
8
It still has some utility, particularly for checking certain deprecations or other
9
decorators which modify the runtime signature where you want the type hint to match
10
the python source rather than runtime signature, perhaps.
11

12
The basic kinds of checks performed are:
13

14
- Set of items defined by the stubs vs implementation
15
  - Missing stub: MISSING_STUB = 1
16
  - Missing implementation: MISSING_IMPL = 2
17
- Signatures of functions/methods
18
  - Positional Only Args: POS_ARGS = 4
19
  - Keyword or Positional Args: ARGS = 8
20
  - Variadic Positional Args: VARARG = 16
21
  - Keyword Only Args: KWARGS = 32
22
  - Variadic Keyword Only Args: VARKWARG = 64
23

24
There are some exceptions to when these are checked:
25

26
- Set checks (MISSING_STUB/MISSING_IMPL) only apply at the module level
27
  - i.e. not for classes
28
  - Inheritance makes the set arithmetic harder when only loading AST
29
  - Attributes also make it more complicated when defined in init
30
- Functions type hinted with ``overload`` are ignored for argument checking
31
  - Usually this means the implementation is less strict in signature but will raise
32
    if an invalid signature is used, type checking allows such errors to be caught by
33
    the type checker instead of at runtime.
34
- Private attribute/functions are ignored
35
  - Not expecting a type hint
36
  - applies to anything beginning, but not ending in ``__``
37
  - If ``__all__`` is defined, also applies to anything not in ``__all__``
38
- Deprecated methods are not checked for missing stub
39
  - Only applies to wholesale deprecation, not deprecation of an individual arg
40
  - Other kinds of deprecations (e.g. argument deletion or rename) the type hint should
41
    match the current python definition line still.
42
      - For renames, the new name is used
43
      - For deletions/make keyword only, it is removed upon expiry
44

45
Usage:
46

47
Currently there is not any argument handling/etc, so all configuration is done in
48
source.
49
Since stubtest has almost completely superseded this script, this is unlikely to change.
50

51
The categories outlined above can each be ignored, and ignoring multiple can be done
52
using the bitwise or (``|``) operator, e.g. ``ARGS | VARKWARG``.
53

54
This can be done globally or on a per file basis, by editing ``per_file_ignore``.
55
For the latter, the key is the Pathlib Path to the affected file, and the value is the
56
integer ignore.
57

58
Must be run from repository root:
59

60
``python tools/check_typehints.py``
61
"""
62

63
import ast
64
import pathlib
65
import sys
66

67
MISSING_STUB = 1
68
MISSING_IMPL = 2
69
POS_ARGS = 4
70
ARGS = 8
71
VARARG = 16
72
KWARGS = 32
73
VARKWARG = 64
74

75

76
def check_file(path, ignore=0):
77
    stubpath = path.with_suffix(".pyi")
78
    ret = 0
79
    if not stubpath.exists():
80
        return 0, 0
81
    tree = ast.parse(path.read_text())
82
    stubtree = ast.parse(stubpath.read_text())
83
    return check_namespace(tree, stubtree, path, ignore)
84

85

86
def check_namespace(tree, stubtree, path, ignore=0):
87
    ret = 0
88
    count = 0
89
    tree_items = set(
90
        i.name
91
        for i in tree.body
92
        if hasattr(i, "name") and (not i.name.startswith("_") or i.name.endswith("__"))
93
    )
94
    stubtree_items = set(
95
        i.name
96
        for i in stubtree.body
97
        if hasattr(i, "name") and (not i.name.startswith("_") or i.name.endswith("__"))
98
    )
99

100
    for item in tree.body:
101
        if isinstance(item, ast.Assign):
102
            tree_items |= set(
103
                i.id
104
                for i in item.targets
105
                if hasattr(i, "id")
106
                and (not i.id.startswith("_") or i.id.endswith("__"))
107
            )
108
            for target in item.targets:
109
                if isinstance(target, ast.Tuple):
110
                    tree_items |= set(i.id for i in target.elts)
111
        elif isinstance(item, ast.AnnAssign):
112
            tree_items |= {item.target.id}
113
    for item in stubtree.body:
114
        if isinstance(item, ast.Assign):
115
            stubtree_items |= set(
116
                i.id
117
                for i in item.targets
118
                if hasattr(i, "id")
119
                and (not i.id.startswith("_") or i.id.endswith("__"))
120
            )
121
            for target in item.targets:
122
                if isinstance(target, ast.Tuple):
123
                    stubtree_items |= set(i.id for i in target.elts)
124
        elif isinstance(item, ast.AnnAssign):
125
            stubtree_items |= {item.target.id}
126

127
    try:
128
        all_ = ast.literal_eval(ast.unparse(get_subtree(tree, "__all__").value))
129
    except ValueError:
130
        all_ = []
131

132
    if all_:
133
        missing = (tree_items - stubtree_items) & set(all_)
134
    else:
135
        missing = tree_items - stubtree_items
136

137
    deprecated = set()
138
    for item_name in missing:
139
        item = get_subtree(tree, item_name)
140
        if hasattr(item, "decorator_list"):
141
            if "deprecated" in [
142
                i.func.attr
143
                for i in item.decorator_list
144
                if hasattr(i, "func") and hasattr(i.func, "attr")
145
            ]:
146
                deprecated |= {item_name}
147

148
    if missing - deprecated and ~ignore & MISSING_STUB:
149
        print(f"{path}: {missing - deprecated} missing from stubs")
150
        ret |= MISSING_STUB
151
        count += 1
152

153
    non_class_or_func = set()
154
    for item_name in stubtree_items - tree_items:
155
        try:
156
            get_subtree(tree, item_name)
157
        except ValueError:
158
            pass
159
        else:
160
            non_class_or_func |= {item_name}
161

162
    missing_implementation = stubtree_items - tree_items - non_class_or_func
163
    if missing_implementation and ~ignore & MISSING_IMPL:
164
        print(f"{path}: {missing_implementation} in stubs and not source")
165
        ret |= MISSING_IMPL
166
        count += 1
167

168
    for item_name in tree_items & stubtree_items:
169
        item = get_subtree(tree, item_name)
170
        stubitem = get_subtree(stubtree, item_name)
171
        if isinstance(item, ast.FunctionDef) and isinstance(stubitem, ast.FunctionDef):
172
            err, c = check_function(item, stubitem, f"{path}::{item_name}", ignore)
173
            ret |= err
174
            count += c
175
        if isinstance(item, ast.ClassDef):
176
            # Ignore set differences for classes... while it would be nice to have
177
            # inheritance and attributes set in init/methods make both presence and
178
            # absence of nodes spurious
179
            err, c = check_namespace(
180
                item,
181
                stubitem,
182
                f"{path}::{item_name}",
183
                ignore | MISSING_STUB | MISSING_IMPL,
184
            )
185
            ret |= err
186
            count += c
187

188
    return ret, count
189

190

191
def check_function(item, stubitem, path, ignore):
192
    ret = 0
193
    count = 0
194

195
    # if the stub calls overload, assume it knows what its doing
196
    overloaded = "overload" in [
197
        i.id for i in stubitem.decorator_list if hasattr(i, "id")
198
    ]
199
    if overloaded:
200
        return 0, 0
201

202
    item_posargs = [a.arg for a in item.args.posonlyargs]
203
    stubitem_posargs = [a.arg for a in stubitem.args.posonlyargs]
204
    if item_posargs != stubitem_posargs and ~ignore & POS_ARGS:
205
        print(
206
            f"{path} {item.name} posargs differ: {item_posargs} vs {stubitem_posargs}"
207
        )
208
        ret |= POS_ARGS
209
        count += 1
210

211
    item_args = [a.arg for a in item.args.args]
212
    stubitem_args = [a.arg for a in stubitem.args.args]
213
    if item_args != stubitem_args and ~ignore & ARGS:
214
        print(f"{path} args differ for {item.name}: {item_args} vs {stubitem_args}")
215
        ret |= ARGS
216
        count += 1
217

218
    item_vararg = item.args.vararg
219
    stubitem_vararg = stubitem.args.vararg
220
    if ~ignore & VARARG:
221
        if (item_vararg is None) ^ (stubitem_vararg is None):
222
            if item_vararg:
223
                print(
224
                    f"{path} {item.name} vararg differ: "
225
                    f"{item_vararg.arg} vs {stubitem_vararg}"
226
                )
227
            else:
228
                print(
229
                    f"{path} {item.name} vararg differ: "
230
                    f"{item_vararg} vs {stubitem_vararg.arg}"
231
                )
232
            ret |= VARARG
233
            count += 1
234
        elif item_vararg is None:
235
            pass
236
        elif item_vararg.arg != stubitem_vararg.arg:
237
            print(
238
                f"{path} {item.name} vararg differ: "
239
                f"{item_vararg.arg} vs {stubitem_vararg.arg}"
240
            )
241
            ret |= VARARG
242
            count += 1
243

244
    item_kwonlyargs = [a.arg for a in item.args.kwonlyargs]
245
    stubitem_kwonlyargs = [a.arg for a in stubitem.args.kwonlyargs]
246
    if item_kwonlyargs != stubitem_kwonlyargs and ~ignore & KWARGS:
247
        print(
248
            f"{path} {item.name} kwonlyargs differ: "
249
            f"{item_kwonlyargs} vs {stubitem_kwonlyargs}"
250
        )
251
        ret |= KWARGS
252
        count += 1
253

254
    item_kwarg = item.args.kwarg
255
    stubitem_kwarg = stubitem.args.kwarg
256
    if ~ignore & VARKWARG:
257
        if (item_kwarg is None) ^ (stubitem_kwarg is None):
258
            if item_kwarg:
259
                print(
260
                    f"{path} {item.name} varkwarg differ: "
261
                    f"{item_kwarg.arg} vs {stubitem_kwarg}"
262
                )
263
            else:
264
                print(
265
                    f"{path} {item.name} varkwarg differ: "
266
                    f"{item_kwarg} vs {stubitem_kwarg.arg}"
267
                )
268
            ret |= VARKWARG
269
            count += 1
270
        elif item_kwarg is None:
271
            pass
272
        elif item_kwarg.arg != stubitem_kwarg.arg:
273
            print(
274
                f"{path} {item.name} varkwarg differ: "
275
                f"{item_kwarg.arg} vs {stubitem_kwarg.arg}"
276
            )
277
            ret |= VARKWARG
278
            count += 1
279

280
    return ret, count
281

282

283
def get_subtree(tree, name):
284
    for item in tree.body:
285
        if isinstance(item, ast.Assign):
286
            if name in [i.id for i in item.targets if hasattr(i, "id")]:
287
                return item
288
            for target in item.targets:
289
                if isinstance(target, ast.Tuple):
290
                    if name in [i.id for i in target.elts]:
291
                        return item
292
        if isinstance(item, ast.AnnAssign):
293
            if name == item.target.id:
294
                return item
295
        if not hasattr(item, "name"):
296
            continue
297
        if item.name == name:
298
            return item
299
    raise ValueError(f"no such item {name} in tree")
300

301

302
if __name__ == "__main__":
303
    out = 0
304
    count = 0
305
    basedir = pathlib.Path("lib/matplotlib")
306
    per_file_ignore = {
307
        # Edge cases for items set via `get_attr`, etc
308
        basedir / "__init__.py": MISSING_IMPL,
309
        # Base class has **kwargs, subclasses have more specific
310
        basedir / "ticker.py": VARKWARG,
311
        basedir / "layout_engine.py": VARKWARG,
312
    }
313
    for f in basedir.rglob("**/*.py"):
314
        err, c = check_file(f, ignore=0 | per_file_ignore.get(f, 0))
315
        out |= err
316
        count += c
317
    print("\n")
318
    print(f"{count} total errors found")
319
    sys.exit(out)
320

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.