build123d/tools/deglob.py

167 lines
5.3 KiB
Python
Executable file

#!/usr/bin/env python
"""
name: deglob.py
by: Gumyr
date: April 12th 2025
desc:
A command-line script (deglob.py) that scans a Python file for references to
symbols from the build123d library and outputs a 'from build123d import ...'
line listing only the symbols that are actually used by that file.
This is useful to replace wildcard imports like 'from build123d import *'
with a more explicit import statement. By relying on Python's AST, this
script can detect which build123d names are referenced, then generate
an import statement listing only those names. This practice can help
prevent polluting the global namespace and improve clarity.
Example:
deglob.py my_build123d_script.py
After parsing my_build123d_script.py, the script prints a line such as:
from build123d import Workplane, Solid
Which you can then paste back into the file to replace the glob import.
Module Contents:
- parse_args(): Parse the command-line argument for the input file path.
- find_used_symbols(): Parse Python source code to find referenced names.
- main(): Orchestrates reading the file, analyzing symbols, and printing
the replacement import line.
license:
Copyright 2025 Gumyr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import argparse
import ast
import sys
from pathlib import Path
import build123d
def parse_args():
"""
Parse command-line arguments for the deglob tool.
Returns:
argparse.Namespace: An object containing the parsed command-line arguments:
- build123d_file (Path): Path to the input build123dO file.
"""
parser = argparse.ArgumentParser(
description="Find all the build123d symbols in module."
)
# Required positional argument
parser.add_argument("build123d_file", type=Path, help="Path to the build123d file")
args = parser.parse_args()
return args
def find_used_symbols(source_code: str) -> set[str]:
"""find_used_symbols
Extract all of the symbols from the source code into a set of strings.
Args:
source_code (str): contents of build123d program
Returns:
set[str]: extracted symbols
"""
tree = ast.parse(source_code)
# Is the glob import from build123d used?
from_glob_import = any(
isinstance(node, ast.ImportFrom)
and node.module == "build123d"
and any(alias.name == "*" for alias in node.names)
for node in ast.walk(tree)
)
if not from_glob_import:
print("Glob import from build123d not found")
sys.exit(0)
symbols = set()
# Create a custom version of visit_Name that records the symbol
class SymbolFinder(ast.NodeVisitor):
def visit_Name(self, node):
# node.id is the variable name or symbol
symbols.add(node.id)
self.generic_visit(node)
SymbolFinder().visit(tree)
return symbols
def main():
"""
Main entry point for the deglob script.
Steps:
1. Parse and validate command-line arguments for the target Python file.
2. Read the file's source code.
3. Use an AST-based check to confirm whether there is at least one
'from build123d import *' statement in the code.
4. Collect all referenced symbol names from the file's abstract syntax tree.
5. Intersect these names with those found in build123d.__all__ to identify
which build123d symbols are actually used.
6. Print an import statement that explicitly imports only the used symbols.
Behavior:
- If no 'from build123d import *' import is found, the script prints
a message and exits.
- If multiple glob imports appear, only a single explicit import line
is generated regardless of the number of glob imports in the file.
- Pre-existing non-glob imports are left unchanged in the user's code;
they may result in redundant imports if the user chooses to keep them.
Raises:
SystemExit: If the file does not exist or if a glob import statement
isn't found.
"""
# Get the command line arguments
args = parse_args()
# Check that the build123d file is valid
if not args.build123d_file.exists():
print(f"Error: file not found - {args.build123d_file}", file=sys.stderr)
sys.exit(1)
# Read the code
with open(args.build123d_file, "r", encoding="utf-8") as f:
code = f.read()
# Check for the glob import and extract the symbols
used_symbols = find_used_symbols(code)
# Find the imported build123d symbols
actual_imports = sorted(used_symbols.intersection(set(build123d.__all__)))
# Create the import statement to replace the glob import
import_line = f"from build123d import {', '.join(actual_imports)}"
print(import_line)
if __name__ == "__main__":
main()