Last active
June 19, 2025 01:08
-
-
Save hiranp/8bb28e882d6884338ea50fa17d923764 to your computer and use it in GitHub Desktop.
Convert Conda requirments.txt to uv pyproject.toml
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 conda environment.yml or requirements.txt to uv-compatible pyproject.toml format | |
| This script reads a conda environment file (environment.yml or requirements.txt with conda format) | |
| and converts all dependencies to a format compatible with uv in pyproject.toml. | |
| Features: | |
| - Extracts conda and pip dependencies from environment files | |
| - Preserves version information from the source file | |
| - Converts version constraints to '>=' format for better compatibility with uv | |
| - Filters out internal conda packages (starting with _) | |
| - Adds dependencies to pyproject.toml under [project.dependencies] | |
| REQUIRES: | |
| - pyyaml | |
| - toml | |
| INSTALLATION: | |
| pip install pyyaml toml | |
| EXECUTION: | |
| python convert_requirements.py [requirements.txt | environment.yml] | |
| EXAMPLE: | |
| python convert_requirements.py requirements.txt | |
| python convert_requirements.py environment.yml | |
| """ | |
| import yaml | |
| import toml | |
| import os | |
| import sys | |
| def parse_requirements_txt(filename): | |
| """ | |
| Parse a requirements.txt or environment.yml file to extract dependencies | |
| """ | |
| conda_dependencies = [] | |
| pip_dependencies = [] | |
| with open(filename, "r") as file: | |
| content = file.read() | |
| if filename.endswith(".yml") or filename.endswith(".yaml"): | |
| # Parse YAML content for environment.yml | |
| try: | |
| env_yaml = yaml.safe_load(content) | |
| if "dependencies" in env_yaml: | |
| for dep in env_yaml["dependencies"]: | |
| if isinstance(dep, dict) and "pip" in dep: | |
| # Handle pip dependencies inside conda env file | |
| for pip_dep in dep["pip"]: | |
| pip_dependencies.append(pip_dep) | |
| elif isinstance(dep, str) and not dep.startswith("python="): | |
| # Keep conda dependencies with their versions, but skip python version | |
| if "=" in dep: | |
| package_parts = dep.split("=") | |
| if not package_parts[0].startswith( | |
| "_" | |
| ): # Skip internal packages | |
| conda_dependencies.append(dep) | |
| else: | |
| conda_dependencies.append(dep) | |
| except yaml.YAMLError as e: | |
| print(f"Error parsing YAML: {e}") | |
| return [], [] | |
| else: | |
| # Assume requirements.txt format | |
| lines = content.split("\n") | |
| # Check if it contains conda-style entries | |
| if any( | |
| line.startswith("name:") or line.startswith("channels:") | |
| for line in lines | |
| ): | |
| # This is a conda env file saved as .txt | |
| in_pip_section = False | |
| in_deps_section = False | |
| for line in lines: | |
| line = line.strip() | |
| if line.startswith("dependencies:"): | |
| in_deps_section = True | |
| in_pip_section = False | |
| continue | |
| if in_deps_section and line.startswith("- pip:"): | |
| in_pip_section = True | |
| in_deps_section = False | |
| continue | |
| if in_deps_section and line.startswith("-"): | |
| dep = line[1:].strip() | |
| if not dep.startswith("python="): # Skip Python version | |
| if "=" in dep: | |
| package_parts = dep.split("=") | |
| if not package_parts[0].startswith( | |
| "_" | |
| ): # Skip internal packages | |
| conda_dependencies.append(dep) | |
| else: | |
| if not dep.startswith("_"): # Skip internal packages | |
| conda_dependencies.append(dep) | |
| if in_pip_section and line.startswith("-"): | |
| pip_dep = line[1:].strip() | |
| pip_dependencies.append(pip_dep) | |
| else: | |
| # Regular pip requirements.txt | |
| for line in lines: | |
| line = line.strip() | |
| if line and not line.startswith("#"): | |
| pip_dependencies.append(line) | |
| # Format conda dependencies to PyPI format | |
| formatted_conda_deps = [] | |
| for dep in conda_dependencies: | |
| if "=" in dep: | |
| parts = dep.split("=") | |
| name = parts[0] | |
| if len(parts) >= 2: | |
| version = parts[1] | |
| # Convert conda version constraint to PyPI format | |
| formatted_conda_deps.append(f"{name}=={version}") | |
| else: | |
| formatted_conda_deps.append(name) | |
| else: | |
| formatted_conda_deps.append(dep) | |
| # Keep pip dependencies with their exact version constraints | |
| formatted_pip_deps = [] | |
| for dep in pip_dependencies: | |
| formatted_pip_deps.append(dep) | |
| return formatted_conda_deps, formatted_pip_deps | |
| def update_pyproject_toml(pyproject_path, conda_deps, pip_deps): | |
| """ | |
| Update or create a pyproject.toml file with the extracted dependencies | |
| """ | |
| pyproject_data = {} | |
| # Load existing pyproject.toml if it exists | |
| if os.path.exists(pyproject_path): | |
| with open(pyproject_path, "r") as file: | |
| try: | |
| pyproject_data = toml.load(file) | |
| except toml.TomlDecodeError as e: | |
| print(f"Error parsing existing pyproject.toml: {e}") | |
| pyproject_data = {} | |
| # Initialize project section if it doesn't exist | |
| if "project" not in pyproject_data: | |
| pyproject_data["project"] = {} | |
| # Update dependencies list | |
| if "dependencies" not in pyproject_data["project"]: | |
| pyproject_data["project"]["dependencies"] = [] | |
| # Combine and format dependencies for pyproject.toml | |
| all_deps = [] | |
| # Process conda dependencies | |
| for dep in conda_deps: | |
| if dep: | |
| # Format dependency for pyproject.toml using >= instead of == for better compatibility | |
| if "==" in dep: | |
| name, version = dep.split("==", 1) | |
| all_deps.append(f"{name}>={version}") | |
| elif "=" in dep: | |
| # Convert conda version format to PyPI format with >= | |
| name, version = dep.split("=", 1) | |
| all_deps.append(f"{name}>={version}") | |
| else: | |
| all_deps.append(dep) | |
| # Process pip dependencies - convert == to >= for better compatibility | |
| for dep in pip_deps: | |
| if dep: | |
| if "==" in dep: | |
| name, version = dep.split("==", 1) | |
| all_deps.append(f"{name}>={version}") | |
| else: | |
| all_deps.append(dep) | |
| # Update dependencies - We'll handle writing them in a custom way | |
| pyproject_data["project"]["dependencies"] = all_deps | |
| # Add tool.uv section | |
| if "tool" not in pyproject_data: | |
| pyproject_data["tool"] = {} | |
| if "uv" not in pyproject_data["tool"]: | |
| pyproject_data["tool"]["uv"] = {} | |
| # Write updated pyproject.toml with proper multi-line formatting for dependencies | |
| with open(pyproject_path, "w") as file: | |
| # Get all non-dependencies sections first | |
| temp_data = pyproject_data.copy() | |
| dependencies = temp_data["project"].pop("dependencies") | |
| # Write the non-dependencies parts | |
| for section, content in temp_data.items(): | |
| if section == "project": | |
| file.write(f"[{section}]\n") | |
| for key, value in content.items(): | |
| if isinstance(value, str): | |
| file.write(f'{key} = "{value}"\n') | |
| else: | |
| file.write(f"{key} = {value}\n") | |
| # Write dependencies with proper formatting | |
| file.write("dependencies = [\n") | |
| for dep in sorted(dependencies): | |
| file.write(f' "{dep}",\n') | |
| file.write("]\n\n") | |
| else: | |
| file.write(f"[{section}]\n") | |
| for key, value in content.items(): | |
| file.write(f"{key} = {value}\n") | |
| file.write("\n") | |
| print(f"Updated {pyproject_path} with {len(all_deps)} dependencies") | |
| def generate_requirements_txt(deps, output_file): | |
| """ | |
| Generate a requirements.txt file from a list of dependencies | |
| """ | |
| # Sort dependencies alphabetically for better readability | |
| sorted_deps = sorted(deps) | |
| with open(output_file, "w") as f: | |
| for dep in sorted_deps: | |
| f.write(f"{dep}\n") | |
| print(f"Generated {output_file} with {len(deps)} dependencies") | |
| def main(): | |
| # Parse command line arguments | |
| import argparse | |
| parser = argparse.ArgumentParser( | |
| description="Convert conda environment to uv-compatible pyproject.toml" | |
| ) | |
| parser.add_argument( | |
| "input_file", | |
| nargs="?", | |
| help="Input environment.yml or requirements.txt file (default: auto-detect)", | |
| ) | |
| parser.add_argument( | |
| "-o", | |
| "--output", | |
| default="pyproject.toml", | |
| help="Output pyproject.toml file (default: pyproject.toml)", | |
| ) | |
| parser.add_argument( | |
| "--requirements", | |
| action="store_true", | |
| help="Also generate a requirements.txt file", | |
| ) | |
| args = parser.parse_args() | |
| # Determine input file | |
| requirements_file = args.input_file | |
| if not requirements_file: | |
| # Default to looking for requirements.txt or environment.yml | |
| if os.path.exists("requirements.txt"): | |
| requirements_file = "requirements.txt" | |
| elif os.path.exists("environment.yml"): | |
| requirements_file = "environment.yml" | |
| else: | |
| print("Error: Could not find requirements.txt or environment.yml") | |
| return 1 | |
| pyproject_path = args.output | |
| print(f"Converting {requirements_file} to {pyproject_path}") | |
| conda_deps, pip_deps = parse_requirements_txt(requirements_file) | |
| # Print found dependencies for debugging | |
| print("Conda dependencies:") | |
| for dep in conda_deps: | |
| print(f" - {dep}") | |
| print("\nPip dependencies:") | |
| for dep in pip_deps: | |
| print(f" - {dep}") | |
| # Update pyproject.toml | |
| update_pyproject_toml(pyproject_path, conda_deps, pip_deps) | |
| # Generate requirements.txt if requested | |
| if args.requirements: | |
| # Combine all dependencies | |
| all_deps = [] | |
| for dep in conda_deps + pip_deps: | |
| if "==" in dep: | |
| # Convert to >= for requirements-uv.txt to match pyproject.toml | |
| name, version = dep.split("==", 1) | |
| all_deps.append(f"{name}>={version}") | |
| elif ">=" in dep: | |
| # Keep the >= format | |
| all_deps.append(dep) | |
| else: | |
| all_deps.append(dep) | |
| # Generate requirements.txt | |
| generate_requirements_txt(all_deps, "requirements-uv.txt") | |
| print( | |
| f"\nFound {len(conda_deps)} conda dependencies and {len(pip_deps)} pip dependencies" | |
| ) | |
| print(f"Updated {pyproject_path} successfully!") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment