Skip to content

Instantly share code, notes, and snippets.

@hiranp
Last active June 19, 2025 01:08
Show Gist options
  • Save hiranp/8bb28e882d6884338ea50fa17d923764 to your computer and use it in GitHub Desktop.
Save hiranp/8bb28e882d6884338ea50fa17d923764 to your computer and use it in GitHub Desktop.

Revisions

  1. hiranp revised this gist Jun 19, 2025. No changes.
  2. hiranp revised this gist Jun 19, 2025. 1 changed file with 172 additions and 38 deletions.
    210 changes: 172 additions & 38 deletions convert_requirements.py
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,30 @@
    #!/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
    @@ -13,7 +37,7 @@ def parse_requirements_txt(filename):
    """
    Parse a requirements.txt or environment.yml file to extract dependencies
    """
    dependencies = []
    conda_dependencies = []
    pip_dependencies = []

    with open(filename, "r") as file:
    @@ -29,17 +53,16 @@ def parse_requirements_txt(filename):
    # 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("_")
    and not dep.startswith("python=")
    ):
    # Only add non-internal conda packages (not starting with _)
    # and skip python version specification
    package = dep.split("=")[
    0
    ] # Extract package name without version
    dependencies.append(package)
    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 [], []
    @@ -71,11 +94,16 @@ def parse_requirements_txt(filename):

    if in_deps_section and line.startswith("-"):
    dep = line[1:].strip()
    if not dep.startswith("_") and not dep.startswith("python="):
    package = dep.split("=")[
    0
    ] # Extract package name without version
    dependencies.append(package)
    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()
    @@ -87,17 +115,27 @@ def parse_requirements_txt(filename):
    if line and not line.startswith("#"):
    pip_dependencies.append(line)

    # Clean up version constraints for pip dependencies
    cleaned_pip_deps = []
    for dep in pip_dependencies:
    # Convert == to >=
    if "==" in dep:
    name, version = dep.split("==", 1)
    cleaned_pip_deps.append(f"{name}>={version}")
    # 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:
    cleaned_pip_deps.append(dep)
    formatted_conda_deps.append(dep)

    return dependencies, cleaned_pip_deps
    # 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):
    @@ -123,17 +161,33 @@ def update_pyproject_toml(pyproject_path, conda_deps, pip_deps):
    if "dependencies" not in pyproject_data["project"]:
    pyproject_data["project"]["dependencies"] = []

    # Combine conda and pip dependencies
    # Combine and format dependencies for pyproject.toml
    all_deps = []

    # Process conda dependencies
    for dep in conda_deps:
    if dep and dep not in all_deps:
    all_deps.append(dep)
    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 and dep not in all_deps:
    all_deps.append(dep)
    if dep:
    if "==" in dep:
    name, version = dep.split("==", 1)
    all_deps.append(f"{name}>={version}")
    else:
    all_deps.append(dep)

    # Update dependencies
    # Update dependencies - We'll handle writing them in a custom way
    pyproject_data["project"]["dependencies"] = all_deps

    # Add tool.uv section
    @@ -143,17 +197,78 @@ def update_pyproject_toml(pyproject_path, conda_deps, pip_deps):
    if "uv" not in pyproject_data["tool"]:
    pyproject_data["tool"]["uv"] = {}

    # Write updated pyproject.toml
    # Write updated pyproject.toml with proper multi-line formatting for dependencies
    with open(pyproject_path, "w") as file:
    toml.dump(pyproject_data, 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():
    if len(sys.argv) > 1:
    requirements_file = sys.argv[1]
    else:
    # 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"
    @@ -163,7 +278,7 @@ def main():
    print("Error: Could not find requirements.txt or environment.yml")
    return 1

    pyproject_path = "pyproject.toml"
    pyproject_path = args.output

    print(f"Converting {requirements_file} to {pyproject_path}")

    @@ -178,8 +293,27 @@ def main():
    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"
    )
  3. hiranp created this gist Jun 19, 2025.
    191 changes: 191 additions & 0 deletions convert_requirements.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,191 @@
    #!/usr/bin/env python3
    """
    Script to convert conda environment.yml or requirements.txt to uv-compatible pyproject.toml format
    """

    import yaml
    import toml
    import os
    import sys


    def parse_requirements_txt(filename):
    """
    Parse a requirements.txt or environment.yml file to extract dependencies
    """
    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("_")
    and not dep.startswith("python=")
    ):
    # Only add non-internal conda packages (not starting with _)
    # and skip python version specification
    package = dep.split("=")[
    0
    ] # Extract package name without version
    dependencies.append(package)
    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("_") and not dep.startswith("python="):
    package = dep.split("=")[
    0
    ] # Extract package name without version
    dependencies.append(package)

    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)

    # Clean up version constraints for pip dependencies
    cleaned_pip_deps = []
    for dep in pip_dependencies:
    # Convert == to >=
    if "==" in dep:
    name, version = dep.split("==", 1)
    cleaned_pip_deps.append(f"{name}>={version}")
    else:
    cleaned_pip_deps.append(dep)

    return dependencies, cleaned_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 conda and pip dependencies
    all_deps = []
    for dep in conda_deps:
    if dep and dep not in all_deps:
    all_deps.append(dep)

    for dep in pip_deps:
    if dep and dep not in all_deps:
    all_deps.append(dep)

    # Update dependencies
    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 open(pyproject_path, "w") as file:
    toml.dump(pyproject_data, file)

    print(f"Updated {pyproject_path} with {len(all_deps)} dependencies")


    def main():
    if len(sys.argv) > 1:
    requirements_file = sys.argv[1]
    else:
    # 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 = "pyproject.toml"

    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(pyproject_path, conda_deps, pip_deps)

    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())