Skip to content

Instantly share code, notes, and snippets.

@Sorcher
Created November 1, 2024 13:52
Show Gist options
  • Save Sorcher/8bf32c6fca74b02af7af8a1c319026e4 to your computer and use it in GitHub Desktop.
Save Sorcher/8bf32c6fca74b02af7af8a1c319026e4 to your computer and use it in GitHub Desktop.
# _______________________
# 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