|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +import ast |
| 4 | +import pathlib |
| 5 | +import sys |
| 6 | + |
| 7 | +MISSING_STUB = 1 |
| 8 | +MISSING_IMPL = 2 |
| 9 | +POS_ARGS = 4 |
| 10 | +ARGS = 8 |
| 11 | +VARARG = 16 |
| 12 | +KWARGS = 32 |
| 13 | +VARKWARG = 64 |
| 14 | + |
| 15 | + |
| 16 | +def check_file(path, ignore=0): |
| 17 | + stubpath = path.with_suffix(".pyi") |
| 18 | + ret = 0 |
| 19 | + if not stubpath.exists(): |
| 20 | + return 0, 0 |
| 21 | + tree = ast.parse(path.read_text()) |
| 22 | + stubtree = ast.parse(stubpath.read_text()) |
| 23 | + return check_namespace(tree, stubtree, path, ignore) |
| 24 | + |
| 25 | + |
| 26 | +def check_namespace(tree, stubtree, path, ignore=0): |
| 27 | + ret = 0 |
| 28 | + count = 0 |
| 29 | + tree_items = set( |
| 30 | + i.name for i in tree.body if hasattr(i, "name") and not i.name.startswith("_") |
| 31 | + ) |
| 32 | + stubtree_items = set( |
| 33 | + i.name |
| 34 | + for i in stubtree.body |
| 35 | + if hasattr(i, "name") and not i.name.startswith("_") |
| 36 | + ) |
| 37 | + |
| 38 | + for item in tree.body: |
| 39 | + if isinstance(item, ast.Assign): |
| 40 | + tree_items |= set( |
| 41 | + i.id |
| 42 | + for i in item.targets |
| 43 | + if hasattr(i, "id") and not i.id.startswith("_") |
| 44 | + ) |
| 45 | + for target in item.targets: |
| 46 | + if isinstance(target, ast.Tuple): |
| 47 | + tree_items |= set(i.id for i in target.elts) |
| 48 | + elif isinstance(item, ast.AnnAssign): |
| 49 | + tree_items |= {item.target.id} |
| 50 | + for item in stubtree.body: |
| 51 | + if isinstance(item, ast.Assign): |
| 52 | + stubtree_items |= set( |
| 53 | + i.id |
| 54 | + for i in item.targets |
| 55 | + if hasattr(i, "id") and not i.id.startswith("_") |
| 56 | + ) |
| 57 | + for target in item.targets: |
| 58 | + if isinstance(target, ast.Tuple): |
| 59 | + stubtree_items |= set(i.id for i in target.elts) |
| 60 | + elif isinstance(item, ast.AnnAssign): |
| 61 | + stubtree_items |= {item.target.id} |
| 62 | + |
| 63 | + try: |
| 64 | + all_ = ast.literal_eval(ast.unparse(get_subtree(tree, "__all__").value)) |
| 65 | + except ValueError: |
| 66 | + all_ = [] |
| 67 | + |
| 68 | + if all_: |
| 69 | + missing = (tree_items - stubtree_items) & set(all_) |
| 70 | + else: |
| 71 | + missing = tree_items - stubtree_items |
| 72 | + |
| 73 | + deprecated = set() |
| 74 | + for item_name in missing: |
| 75 | + item = get_subtree(tree, item_name) |
| 76 | + if hasattr(item, "decorator_list"): |
| 77 | + if "deprecated" in [ |
| 78 | + i.func.attr |
| 79 | + for i in item.decorator_list |
| 80 | + if hasattr(i, "func") and hasattr(i.func, "attr") |
| 81 | + ]: |
| 82 | + deprecated |= {item_name} |
| 83 | + |
| 84 | + if missing - deprecated and ~ignore & MISSING_STUB: |
| 85 | + print(f"{path}: {missing - deprecated} missing from stubs") |
| 86 | + ret |= MISSING_STUB |
| 87 | + count += 1 |
| 88 | + |
| 89 | + non_class_or_func = set() |
| 90 | + for item_name in stubtree_items - tree_items: |
| 91 | + try: |
| 92 | + get_subtree(tree, item_name) |
| 93 | + except ValueError: |
| 94 | + pass |
| 95 | + else: |
| 96 | + non_class_or_func |= {item_name} |
| 97 | + |
| 98 | + missing_implementation = stubtree_items - tree_items - non_class_or_func |
| 99 | + if missing_implementation and ~ignore & MISSING_IMPL: |
| 100 | + print( |
| 101 | + f"{path}: {missing_implementation} in stubs and not source" |
| 102 | + ) |
| 103 | + ret |= MISSING_IMPL |
| 104 | + count += 1 |
| 105 | + |
| 106 | + for item_name in tree_items & stubtree_items: |
| 107 | + item = get_subtree(tree, item_name) |
| 108 | + stubitem = get_subtree(stubtree, item_name) |
| 109 | + if isinstance(item, ast.FunctionDef) and isinstance(stubitem, ast.FunctionDef): |
| 110 | + err, c = check_function(item, stubitem, f"{path}::{item_name}", ignore) |
| 111 | + ret |= err |
| 112 | + count += c |
| 113 | + if isinstance(item, ast.ClassDef): |
| 114 | + # Ignore set differences for classes... while it would be nice to have |
| 115 | + # inheritance and attributes set in init/methods make both presence and |
| 116 | + # absence of nodes spurious |
| 117 | + err, c = check_namespace( |
| 118 | + item, |
| 119 | + stubitem, |
| 120 | + f"{path}::{item_name}", |
| 121 | + ignore | MISSING_STUB | MISSING_IMPL, |
| 122 | + ) |
| 123 | + ret |= err |
| 124 | + count += c |
| 125 | + |
| 126 | + return ret, count |
| 127 | + |
| 128 | + |
| 129 | +def check_function(item, stubitem, path, ignore): |
| 130 | + ret = 0 |
| 131 | + count = 0 |
| 132 | + |
| 133 | + # if the stub calls overload, assume it knows what its doing |
| 134 | + overloaded = "overload" in [ |
| 135 | + i.id for i in stubitem.decorator_list if hasattr(i, "id") |
| 136 | + ] |
| 137 | + if overloaded: |
| 138 | + return 0, 0 |
| 139 | + |
| 140 | + item_posargs = [a.arg for a in item.args.posonlyargs] |
| 141 | + stubitem_posargs = [a.arg for a in stubitem.args.posonlyargs] |
| 142 | + if item_posargs != stubitem_posargs and ~ignore & POS_ARGS: |
| 143 | + print( |
| 144 | + f"{path} {item.name} posargs differ: {item_posargs} vs {stubitem_posargs}" |
| 145 | + ) |
| 146 | + ret |= POS_ARGS |
| 147 | + count += 1 |
| 148 | + |
| 149 | + item_args = [a.arg for a in item.args.args] |
| 150 | + stubitem_args = [a.arg for a in stubitem.args.args] |
| 151 | + if item_args != stubitem_args and ~ignore & ARGS: |
| 152 | + print(f"{path} args differ for {item.name}: {item_args} vs {stubitem_args}") |
| 153 | + ret |= ARGS |
| 154 | + count += 1 |
| 155 | + |
| 156 | + item_vararg = item.args.vararg |
| 157 | + stubitem_vararg = stubitem.args.vararg |
| 158 | + if ~ignore & VARARG: |
| 159 | + if (item_vararg is None) ^ (stubitem_vararg is None): |
| 160 | + if item_vararg: |
| 161 | + print( |
| 162 | + f"{path} {item.name} vararg differ: " |
| 163 | + f"{item_vararg.arg} vs {stubitem_vararg}" |
| 164 | + ) |
| 165 | + else: |
| 166 | + print( |
| 167 | + f"{path} {item.name} vararg differ: " |
| 168 | + f"{item_vararg} vs {stubitem_vararg.arg}" |
| 169 | + ) |
| 170 | + ret |= VARARG |
| 171 | + count += 1 |
| 172 | + elif item_vararg is None: |
| 173 | + pass |
| 174 | + elif item_vararg.arg != stubitem_vararg.arg: |
| 175 | + print( |
| 176 | + f"{path} {item.name} vararg differ: " |
| 177 | + f"{item_vararg.arg} vs {stubitem_vararg.arg}" |
| 178 | + ) |
| 179 | + ret |= VARARG |
| 180 | + count += 1 |
| 181 | + |
| 182 | + item_kwonlyargs = [a.arg for a in item.args.kwonlyargs] |
| 183 | + stubitem_kwonlyargs = [a.arg for a in stubitem.args.kwonlyargs] |
| 184 | + if item_kwonlyargs != stubitem_kwonlyargs and ~ignore & KWARGS: |
| 185 | + print( |
| 186 | + f"{path} {item.name} kwonlyargs differ: " |
| 187 | + f"{item_kwonlyargs} vs {stubitem_kwonlyargs}" |
| 188 | + ) |
| 189 | + ret |= KWARGS |
| 190 | + count += 1 |
| 191 | + |
| 192 | + item_kwarg = item.args.kwarg |
| 193 | + stubitem_kwarg = stubitem.args.kwarg |
| 194 | + if ~ignore & VARKWARG: |
| 195 | + if (item_kwarg is None) ^ (stubitem_kwarg is None): |
| 196 | + if item_kwarg: |
| 197 | + print( |
| 198 | + f"{path} {item.name} kwarg differ: " |
| 199 | + f"{item_kwarg.arg} vs {stubitem_kwarg}" |
| 200 | + ) |
| 201 | + else: |
| 202 | + print( |
| 203 | + f"{path} {item.name} kwarg differ: " |
| 204 | + f"{item_kwarg} vs {stubitem_kwarg.arg}" |
| 205 | + ) |
| 206 | + ret |= VARKWARG |
| 207 | + count += 1 |
| 208 | + elif item_kwarg is None: |
| 209 | + pass |
| 210 | + elif item_kwarg.arg != stubitem_kwarg.arg: |
| 211 | + print( |
| 212 | + f"{path} {item.name} kwarg differ: " |
| 213 | + f"{item_kwarg.arg} vs {stubitem_kwarg.arg}" |
| 214 | + ) |
| 215 | + ret |= VARKWARG |
| 216 | + count += 1 |
| 217 | + |
| 218 | + return ret, count |
| 219 | + |
| 220 | + |
| 221 | +def get_subtree(tree, name): |
| 222 | + for item in tree.body: |
| 223 | + if isinstance(item, ast.Assign): |
| 224 | + if name in [i.id for i in item.targets if hasattr(i, "id")]: |
| 225 | + return item |
| 226 | + for target in item.targets: |
| 227 | + if isinstance(target, ast.Tuple): |
| 228 | + if name in [i.id for i in target.elts]: |
| 229 | + return item |
| 230 | + if isinstance(item, ast.AnnAssign): |
| 231 | + if name == item.target.id: |
| 232 | + return item |
| 233 | + if not hasattr(item, "name"): |
| 234 | + continue |
| 235 | + if item.name == name: |
| 236 | + return item |
| 237 | + raise ValueError(f"no such item {name} in tree") |
| 238 | + |
| 239 | + |
| 240 | +if __name__ == "__main__": |
| 241 | + out = 0 |
| 242 | + count = 0 |
| 243 | + basedir = pathlib.Path("lib/matplotlib") |
| 244 | + per_file_ignore = { |
| 245 | + # Edge cases for items set via `get_attr`, etc |
| 246 | + basedir / "__init__.py": MISSING_IMPL, |
| 247 | + # Base class has **kwargs, subclasses have more specific |
| 248 | + basedir / "ticker.py": VARKWARG, |
| 249 | + basedir / "layout_engine.py": VARKWARG, |
| 250 | + } |
| 251 | + for f in basedir.rglob("**/*.py"): |
| 252 | + err, c = check_file(f, ignore=0 | per_file_ignore.get(f, 0)) |
| 253 | + out |= err |
| 254 | + count += c |
| 255 | + print("\n") |
| 256 | + print(f"{count} total errors found") |
| 257 | + sys.exit(out) |
0 commit comments