Created
March 3, 2025 05:20
-
-
Save rosmur/da033d0493bf45bc4776ba86e0ecec6b to your computer and use it in GitHub Desktop.
requirements.txt to uv pyproject.toml dependencies convertor
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
#!/usr/bin/env python3 | |
""" | |
Script to convert requirements.txt to dependencies list for pyproject.toml for uv project | |
Usage: | |
Execute the script with: | |
python3 requirementstxt_to_uv_pyprojecttoml.py path/to/requirements.txt | |
You can add an optional --validate flag after the path/to/requirements.txt fist positional argument to cross check if number of items in the generated dependency list matches the number of items in the requirements.txt. | |
Eg: python3 requirementstxt_to_uv_pyprojecttoml.py path/to/requirements.txt --validate | |
--- | |
Output: | |
The extracted dependency list is printed out to the terminal - copy and paste it to the target pyproject.toml's [project] section. | |
If you add the --validate flag, a check will be run at the end and report if ✅ Validation successful or ❌ Validation failed (with failure mode) | |
Credits: | |
C3.7S-RM | |
""" | |
import sys | |
import re | |
from pathlib import Path | |
def parse_requirements(req_path): | |
"""Parse a requirements.txt file and return a list of requirements.""" | |
requirements = [] | |
with open(req_path, "r") as file: | |
for line in file: | |
# Skip empty lines and comments | |
line = line.strip() | |
if not line or line.startswith("#"): | |
continue | |
# Skip options like --index-url, -f, --find-links, etc. | |
if line.startswith("-"): | |
continue | |
# Handle requirements with comments | |
line = line.split("#")[0].strip() | |
# Skip if empty after removing comments | |
if not line: | |
continue | |
requirements.append(line) | |
return requirements | |
def format_dependency(req): | |
"""Format a single requirement for pyproject.toml in the format 'package>=1.0.0'.""" | |
# Remove any extras and environment markers | |
req = re.sub(r"\[[^\]]+\]", "", req) | |
req = req.split(";")[0].strip() | |
# Extract package name (everything before any version specifier) | |
name_pattern = re.compile(r"^([a-zA-Z0-9_\-\.]+)") | |
name_match = name_pattern.search(req) | |
if not name_match: | |
return None | |
name = name_match.group(1) | |
# Common version specifiers: ==, >=, <=, ~=, !=, >, < | |
version_pattern = re.compile(r"([=<>!~]+\s*[0-9a-zA-Z\.\-\*]+)") | |
version_matches = version_pattern.findall(req) | |
if not version_matches: | |
# No version specified, use any version | |
return f"{name}" | |
# Join all version specifiers directly with the package name | |
# Example: requests>=2.0.0,<3.0.0 becomes "requests>=2.0.0,<3.0.0" | |
versions = ",".join(v.strip() for v in version_matches) | |
return f"{name}{versions}" | |
def generate_dependencies_section(req_path): | |
"""Generate a dependencies list for pyproject.toml from requirements.txt.""" | |
requirements = parse_requirements(req_path) | |
# Format each requirement for pyproject.toml | |
formatted_deps = [] | |
for req in requirements: | |
formatted = format_dependency(req) | |
if formatted: | |
formatted_deps.append(f' "{formatted}",') | |
# Generate the dependencies section | |
if not formatted_deps: | |
deps_section = "dependencies = [\n]" | |
else: | |
deps_section = "dependencies = [\n" | |
deps_section += "\n".join(formatted_deps) | |
deps_section += "\n]" | |
return deps_section | |
def test_conversion(req_path, generated_dependencies): | |
"""Test that the conversion worked correctly by comparing item counts.""" | |
# Get the original requirements count | |
requirements = parse_requirements(req_path) | |
original_count = len(requirements) | |
# Clean up generated dependencies | |
output_lines = [ | |
line | |
for line in generated_dependencies.split("\n") | |
if line.strip() not in ["dependencies = [", "]"] | |
] | |
output_count = len(output_lines) | |
# Compare counts | |
if original_count == output_count: | |
print( | |
f"✅ Validation successful: {original_count} requirements converted to {output_count} dependencies" | |
) | |
return True | |
else: | |
print( | |
f"❌ Validation failed: {original_count} requirements != {output_count} dependencies" | |
) | |
# Print items that might have been skipped or duplicated | |
if original_count > output_count: | |
print("Some requirements may have been skipped. Check for invalid formats.") | |
else: | |
print("Output has more items than input. Check for duplicates.") | |
return False | |
def main(): | |
if len(sys.argv) < 2 or len(sys.argv) > 3: | |
print(f"Usage: {sys.argv[0]} path/to/requirements.txt [--validate]") | |
sys.exit(1) | |
req_path = Path(sys.argv[1]) | |
if not req_path.exists(): | |
print(f"Error: File {req_path} does not exist.") | |
sys.exit(1) | |
# Generate dependencies section | |
dependencies_section = generate_dependencies_section(req_path) | |
print(dependencies_section) | |
# Run test if requested | |
if len(sys.argv) == 3 and sys.argv[2] == "--validate": | |
success = test_conversion(req_path, dependencies_section) | |
if not success: | |
sys.exit(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I think uv add -r requirements.txt does the same!