Created
November 1, 2024 13:52
-
-
Save Sorcher/8bf32c6fca74b02af7af8a1c319026e4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# _______________________ | |
# Just a simple script i use for generating a requirements.txt file per python project.. | |
# _______________________ | |
#Selecting a Python file through a GUI dialog. | |
#Parsing all imports, including those from custom modules. | |
#Excluding standard library modules and custom project modules. | |
#Including only third-party packages with their installed versions. | |
#Handling existing requirements.txt files by creating backups with incremented suffixes. | |
import re | |
import os | |
import sys | |
from tkinter import Tk, filedialog | |
from importlib.metadata import PackageNotFoundError, version | |
def select_python_file(): | |
root = Tk() | |
root.withdraw() # Hide the root window | |
file_path = filedialog.askopenfilename( | |
filetypes=[("Python Files", "*.py")], | |
title="Select a Python file" | |
) | |
root.destroy() | |
return file_path | |
def parse_imports(file_path): | |
with open(file_path, 'r', encoding='utf-8') as file: | |
file_content = file.read() | |
# Regular expressions to find import statements | |
import_pattern = re.compile(r'^\s*import\s+([a-zA-Z_][\w\.]*)', re.MULTILINE) | |
from_import_pattern = re.compile(r'^\s*from\s+([a-zA-Z_][\w\.]*)\s+import\s+', re.MULTILINE) | |
imports = set(import_pattern.findall(file_content)) | |
imports.update(from_import_pattern.findall(file_content)) | |
return imports | |
def get_installed_package_version(package_name): | |
try: | |
return version(package_name) | |
except PackageNotFoundError: | |
return None | |
def is_standard_library(module_name): | |
if hasattr(sys, 'stdlib_module_names'): | |
return module_name.split('.')[0] in sys.stdlib_module_names | |
else: | |
# Fallback for Python versions < 3.10 | |
# You can expand this list as needed | |
stdlib_modules = { | |
'os', 'sys', 're', 'math', 'json', 'datetime', 'time', 'subprocess', | |
'threading', 'logging', 'itertools', 'functools', 'collections', | |
'asyncio', 'typing', 'http', 'urllib', 'socket', 'email', 'sqlite3' | |
} | |
return module_name.split('.')[0] in stdlib_modules | |
def is_custom_module(module_name, project_root): | |
""" | |
Check if the module is a custom module by looking for its file in the project directory. | |
""" | |
parts = module_name.split('.') | |
possible_paths = [ | |
os.path.join(project_root, *parts, '__init__.py'), | |
os.path.join(project_root, *parts) + '.py' | |
] | |
for path in possible_paths: | |
if os.path.isfile(path): | |
return True | |
return False | |
def find_module_file(module_name, project_root): | |
""" | |
Find the file path of a custom module. | |
""" | |
parts = module_name.split('.') | |
# Attempt to find package (__init__.py) or module (.py) | |
package_init = os.path.join(project_root, *parts, '__init__.py') | |
if os.path.isfile(package_init): | |
return package_init | |
module_file = os.path.join(project_root, *parts) + '.py' | |
if os.path.isfile(module_file): | |
return module_file | |
return None | |
def backup_existing_requirements(output_path): | |
if os.path.isfile(output_path): | |
base, ext = os.path.splitext(output_path) | |
counter = 1 | |
new_name = f"{base}_old_{counter}{ext}" | |
while os.path.isfile(new_name): | |
counter += 1 | |
new_name = f"{base}_old_{counter}{ext}" | |
os.rename(output_path, new_name) | |
print(f"Existing requirements.txt renamed to {new_name}") | |
def generate_requirements_txt(imports, project_root, output_path): | |
processed_modules = set() | |
third_party_packages = set() | |
def process_module(module_name): | |
if module_name in processed_modules: | |
return | |
processed_modules.add(module_name) | |
if is_standard_library(module_name): | |
return | |
if is_custom_module(module_name, project_root): | |
module_file = find_module_file(module_name, project_root) | |
if module_file: | |
nested_imports = parse_imports(module_file) | |
for nested in nested_imports: | |
# Handle relative imports (e.g., from . import something) | |
if nested.startswith('.'): | |
# Relative import; resolve to absolute | |
current_dir = os.path.dirname(module_file) | |
relative_module = nested.lstrip('.') | |
if relative_module: | |
absolute_module = os.path.basename(current_dir) + '.' + relative_module | |
else: | |
# Importing from the current package | |
absolute_module = os.path.basename(current_dir) | |
process_module(absolute_module) | |
else: | |
process_module(nested) | |
return | |
# If not standard or custom, it's a third-party package | |
top_level = module_name.split('.')[0] | |
third_party_packages.add(top_level) | |
for imp in imports: | |
process_module(imp) | |
# Backup existing requirements.txt if it exists | |
backup_existing_requirements(output_path) | |
# Write to requirements.txt | |
with open(output_path, 'w', encoding='utf-8') as file: | |
for package in sorted(third_party_packages): | |
package_version = get_installed_package_version(package) | |
if package_version: | |
file.write(f"{package}=={package_version}\n") | |
else: | |
file.write(f"{package}\n") | |
print(f"requirements.txt generated at {output_path}") | |
if __name__ == "__main__": | |
file_path = select_python_file() | |
if file_path: | |
project_root = os.path.dirname(file_path) | |
imports = parse_imports(file_path) | |
output_path = os.path.join(project_root, "requirements.txt") | |
generate_requirements_txt(imports, project_root, output_path) | |
else: | |
print("No file selected.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment