|
| 1 | +import ast |
| 2 | +import sys |
| 3 | +from pathlib import Path |
| 4 | + |
| 5 | + |
| 6 | +def get_public_names_from_file(filepath: Path) -> dict[str, Path]: |
| 7 | + with open(filepath) as f: |
| 8 | + tree = ast.parse(f.read()) |
| 9 | + |
| 10 | + public_names = {} |
| 11 | + for node in ast.walk(tree): |
| 12 | + if isinstance(node, ast.ClassDef): |
| 13 | + if not node.name.startswith("_"): |
| 14 | + public_names[node.name] = filepath |
| 15 | + elif isinstance(node, ast.FunctionDef): |
| 16 | + if not node.name.startswith("_"): |
| 17 | + public_names[node.name] = filepath |
| 18 | + |
| 19 | + return public_names |
| 20 | + |
| 21 | + |
| 22 | +def get_all_exports(init_file: Path) -> set[str]: |
| 23 | + with open(init_file) as f: |
| 24 | + tree = ast.parse(f.read()) |
| 25 | + |
| 26 | + for node in ast.walk(tree): |
| 27 | + if isinstance(node, ast.Assign): |
| 28 | + for target in node.targets: |
| 29 | + if isinstance(target, ast.Name) and target.id == "__all__": |
| 30 | + if isinstance(node.value, ast.List): |
| 31 | + return { |
| 32 | + elt.value |
| 33 | + for elt in node.value.elts |
| 34 | + if isinstance(elt, ast.Constant) |
| 35 | + } |
| 36 | + return set() |
| 37 | + |
| 38 | + |
| 39 | +def main(): |
| 40 | + EXCLUDED_NAMES = { |
| 41 | + "QueryParam", # internal |
| 42 | + "MobilityAd", # internal |
| 43 | + "extend", # internal |
| 44 | + "parse", # internal |
| 45 | + "url", # internal |
| 46 | + "get_ad", # used with api.get_ad() |
| 47 | + "search", # used with api.search() |
| 48 | + "search_car", # used with api.search_car() |
| 49 | + "search_boat", # used with api.search_boat() |
| 50 | + "search_mc", # used with api.search_mc() |
| 51 | + } |
| 52 | + |
| 53 | + package_dir = Path("blocket_api") |
| 54 | + init_file = package_dir / "__init__.py" |
| 55 | + |
| 56 | + source_files = [ |
| 57 | + package_dir / "constants.py", |
| 58 | + package_dir / "ad_parser.py", |
| 59 | + package_dir / "blocket.py", |
| 60 | + ] |
| 61 | + |
| 62 | + if not init_file.exists(): |
| 63 | + print(f"Error: {init_file} not found") |
| 64 | + sys.exit(1) |
| 65 | + |
| 66 | + all_public_names = {} |
| 67 | + for source_file in source_files: |
| 68 | + if source_file.exists(): |
| 69 | + names = get_public_names_from_file(source_file) |
| 70 | + all_public_names.update(names) |
| 71 | + else: |
| 72 | + print(f"Warning: {source_file} not found, skipping") |
| 73 | + |
| 74 | + all_exports = get_all_exports(init_file) |
| 75 | + |
| 76 | + missing_from_all = { |
| 77 | + name: filepath |
| 78 | + for name, filepath in all_public_names.items() |
| 79 | + if name not in all_exports and name not in EXCLUDED_NAMES |
| 80 | + } |
| 81 | + |
| 82 | + errors = [] |
| 83 | + |
| 84 | + if missing_from_all: |
| 85 | + for name, filepath in sorted(missing_from_all.items()): |
| 86 | + errors.append(f" {filepath.relative_to(package_dir.parent)}: {name}") |
| 87 | + |
| 88 | + if errors: |
| 89 | + print( |
| 90 | + "Make sure to add new public names to __all__ in __init__.py or exclude it:" |
| 91 | + ) |
| 92 | + print("\n".join(errors)) |
| 93 | + |
| 94 | + sys.exit(1) |
| 95 | + |
| 96 | + print(f"✅ All {len(all_public_names)} public names are in __all__") |
| 97 | + sys.exit(0) |
| 98 | + |
| 99 | + |
| 100 | +if __name__ == "__main__": |
| 101 | + main() |
0 commit comments