packagelister.packagelister
1import ast 2import importlib.metadata 3import sys 4 5from pathier import Pathier, Pathish 6from printbuddies import ProgBar 7 8 9def get_packages_from_source(source: str) -> list[str]: 10 """Scan `source` and extract the names of imported packages/modules.""" 11 tree = ast.parse(source) 12 packages = [] 13 for node in ast.walk(tree): 14 type_ = type(node) 15 package = "" 16 if type_ == ast.Import: 17 package = node.names[0].name 18 elif type_ == ast.ImportFrom: 19 package = node.module 20 if package: 21 if "." in package: 22 package = package[: package.find(".")] 23 packages.append(package) 24 return sorted(list(set(packages))) 25 26 27def remove_builtins(packages: list[str]) -> list[str]: 28 """Remove built in packages/modules from a list of package names.""" 29 builtins = list(sys.stdlib_module_names) 30 return filter(lambda x: x not in builtins, packages) 31 32 33def scan(project_dir: Pathish = None, include_builtins: bool = False) -> dict: 34 """Recursively scans a directory for python files to determine 35 what packages are in use, as well as the version number if applicable. 36 37 Returns a dictionary where the keys are package names and 38 the values are dictionaries with the keys `version` for the version number of the package 39 if there is one (None if there isn't) and `files` for a list of the files that import the package. 40 41 :param project_dir: Can be an absolute or relative path to a directory or a single file (.py). 42 If it is relative, it will be assumed to be relative to the current working directory. 43 If an argument isn't given, the current working directory will be scanned. 44 If the path doesn't exist, an empty dictionary is returned.""" 45 if not project_dir: 46 project_dir = Pathier.cwd() 47 elif type(project_dir) is str or project_dir.is_file(): 48 project_dir = Pathier(project_dir) 49 if not project_dir.is_absolute(): 50 project_dir = project_dir.absolute() 51 52 # Raise error if project_dir doesn't exist 53 if not project_dir.exists(): 54 raise FileNotFoundError( 55 f"Can't scan directory that doesn't exist: {project_dir}" 56 ) 57 # You can scan a non python file one at a time if you reeeally want to. 58 if project_dir.is_file(): 59 files = [project_dir] 60 else: 61 files = list(project_dir.rglob("*.py")) 62 63 bar = ProgBar(len(files), width_ratio=0.33) 64 used_packages = {} 65 for file in files: 66 bar.display(suffix=f"Scanning {file.name}") 67 source = file.read_text(encoding="utf-8") 68 packages = get_packages_from_source(source) 69 if not include_builtins: 70 packages = remove_builtins(packages) 71 for package in packages: 72 if file.with_stem(package) not in files: 73 if ( 74 package in used_packages 75 and str(file) not in used_packages[package]["files"] 76 ): 77 used_packages[package]["files"].append(str(file)) 78 else: 79 try: 80 package_version = importlib.metadata.version(package) 81 except ModuleNotFoundError: 82 package_version = None 83 except Exception as e: 84 print(e) 85 package_version = None 86 used_packages[package] = { 87 "files": [str(file)], 88 "version": package_version, 89 } 90 return used_packages
def
get_packages_from_source(source: str) -> list[str]:
10def get_packages_from_source(source: str) -> list[str]: 11 """Scan `source` and extract the names of imported packages/modules.""" 12 tree = ast.parse(source) 13 packages = [] 14 for node in ast.walk(tree): 15 type_ = type(node) 16 package = "" 17 if type_ == ast.Import: 18 package = node.names[0].name 19 elif type_ == ast.ImportFrom: 20 package = node.module 21 if package: 22 if "." in package: 23 package = package[: package.find(".")] 24 packages.append(package) 25 return sorted(list(set(packages)))
Scan source and extract the names of imported packages/modules.
def
remove_builtins(packages: list[str]) -> list[str]:
28def remove_builtins(packages: list[str]) -> list[str]: 29 """Remove built in packages/modules from a list of package names.""" 30 builtins = list(sys.stdlib_module_names) 31 return filter(lambda x: x not in builtins, packages)
Remove built in packages/modules from a list of package names.
def
scan( project_dir: pathier.pathier.Pathier | pathlib.Path | str = None, include_builtins: bool = False) -> dict:
34def scan(project_dir: Pathish = None, include_builtins: bool = False) -> dict: 35 """Recursively scans a directory for python files to determine 36 what packages are in use, as well as the version number if applicable. 37 38 Returns a dictionary where the keys are package names and 39 the values are dictionaries with the keys `version` for the version number of the package 40 if there is one (None if there isn't) and `files` for a list of the files that import the package. 41 42 :param project_dir: Can be an absolute or relative path to a directory or a single file (.py). 43 If it is relative, it will be assumed to be relative to the current working directory. 44 If an argument isn't given, the current working directory will be scanned. 45 If the path doesn't exist, an empty dictionary is returned.""" 46 if not project_dir: 47 project_dir = Pathier.cwd() 48 elif type(project_dir) is str or project_dir.is_file(): 49 project_dir = Pathier(project_dir) 50 if not project_dir.is_absolute(): 51 project_dir = project_dir.absolute() 52 53 # Raise error if project_dir doesn't exist 54 if not project_dir.exists(): 55 raise FileNotFoundError( 56 f"Can't scan directory that doesn't exist: {project_dir}" 57 ) 58 # You can scan a non python file one at a time if you reeeally want to. 59 if project_dir.is_file(): 60 files = [project_dir] 61 else: 62 files = list(project_dir.rglob("*.py")) 63 64 bar = ProgBar(len(files), width_ratio=0.33) 65 used_packages = {} 66 for file in files: 67 bar.display(suffix=f"Scanning {file.name}") 68 source = file.read_text(encoding="utf-8") 69 packages = get_packages_from_source(source) 70 if not include_builtins: 71 packages = remove_builtins(packages) 72 for package in packages: 73 if file.with_stem(package) not in files: 74 if ( 75 package in used_packages 76 and str(file) not in used_packages[package]["files"] 77 ): 78 used_packages[package]["files"].append(str(file)) 79 else: 80 try: 81 package_version = importlib.metadata.version(package) 82 except ModuleNotFoundError: 83 package_version = None 84 except Exception as e: 85 print(e) 86 package_version = None 87 used_packages[package] = { 88 "files": [str(file)], 89 "version": package_version, 90 } 91 return used_packages
Recursively scans a directory for python files to determine what packages are in use, as well as the version number if applicable.
Returns a dictionary where the keys are package names and
the values are dictionaries with the keys version for the version number of the package
if there is one (None if there isn't) and files for a list of the files that import the package.
Parameters
- project_dir: Can be an absolute or relative path to a directory or a single file (.py). If it is relative, it will be assumed to be relative to the current working directory. If an argument isn't given, the current working directory will be scanned. If the path doesn't exist, an empty dictionary is returned.