Skip to content

Instantly share code, notes, and snippets.

@andrader
Forked from arnaldojvg/convert_pipfile_to_uv.py
Last active April 8, 2025 01:40
Show Gist options
  • Save andrader/a020beb04f4366eff14aec12fb26d75a to your computer and use it in GitHub Desktop.
Save andrader/a020beb04f4366eff14aec12fb26d75a to your computer and use it in GitHub Desktop.
# /// script
# dependencies = [
# "toml",
# "rich",
# ]
# ///
# To run the script use `uv run convert_pipfile_to_uv.py` (this way it automagically installs the dependencies)
import re
from pathlib import Path
from urllib.parse import urlparse, urlunparse
import toml
from rich import print
from rich.prompt import Confirm, Prompt
PIPFILE_TO_UV_DEP_NAMES = {
"packages": "dependencies",
"dev-packages": "dev-dependencies",
}
# Override toml.TomlEncoder.dump_list to add newline and indentation
def _dump_list(self, v):
sep = ","
indentation = 4
NL = "\n"
INDENT = " " * indentation
s = (
"["
+ NL
+ INDENT
+ (sep + NL + INDENT).join([str(self.dump_value(u)) for u in v])
+ (sep + NL)
+ "]"
)
return s
toml.TomlEncoder.dump_list = _dump_list
def strip_url_credentials(url: str) -> str:
"""
Remove username and password from URL if present.
"""
parsed = urlparse(url)
# Create new netloc without credentials
netloc = parsed.hostname
if parsed.port:
netloc = f"{netloc}:{parsed.port}"
# Reconstruct URL without credentials
clean_url = urlunparse(
(
parsed.scheme,
netloc,
parsed.path,
parsed.params,
parsed.query,
parsed.fragment,
)
)
return clean_url
def dump_custom_version(package: str, version: dict) -> str:
"""
Handle custom version formats (e.g., git, ref, index).
"""
if "extras" in version:
extras_str = ",".join(version["extras"])
package = f"{package}[{extras_str}]"
if "git" in version:
git_url = version["git"]
ref = version.get("ref", "main")
return f"{package} @ {git_url}@{ref}"
if "version" in version:
return f"{package}{version['version']}"
return package
def convert_pipfile_to_pyproject(
pipfile_path: Path,
output_path: Path,
project_name: str,
project_version: str,
project_description: str,
python_version: str = "pipfile",
):
# Load the Pipfile
with open(pipfile_path) as pipfile:
pipfile_data = toml.load(pipfile)
pyproject_data = {
"project": {
"name": project_name,
"version": project_version,
"dependencies": [],
"dev-dependencies": [], # tmp for easier map
},
"tool": {"uv": {"dev-dependencies": [], "sources": {}}},
}
if project_description:
pyproject_data["project"]["description"] = project_description
if python_version == "pipfile" and "python_version" in (
pipfile_requires := pipfile_data.get("requires", {})
):
python_version = pipfile_requires["python_version"]
if python_version:
python_version = (
f">={python_version}" if python_version[0].isdigit() else python_version
)
pyproject_data["project"]["requires-python"] = python_version
# Handle sources from Pipfile
if "source" in pipfile_data:
pyproject_data["tool"]["uv"]["index"] = []
for source in pipfile_data["source"]:
# Create source entry with cleaned URL (no credentials)
source_entry = {
"name": source["name"].replace("-", "_"),
"url": strip_url_credentials(source["url"]),
}
pyproject_data["tool"]["uv"]["index"].append(source_entry)
# Track packages with custom index
packages_with_index = {}
# Convert dependencies and handle index-specific packages
for pipfile_name, uv_name in PIPFILE_TO_UV_DEP_NAMES.items():
if pipfile_name in pipfile_data:
for package, version in pipfile_data[pipfile_name].items():
if isinstance(version, str):
if version == "*":
pyproject_data["project"][uv_name].append(f"{package}")
else:
pyproject_data["project"][uv_name].append(f"{package}{version}")
elif isinstance(version, dict):
# Check for index specification
if "index" in version:
packages_with_index[package] = {"index": version["index"]}
# Add custom version formats
pyproject_data["project"][uv_name].append(
dump_custom_version(package, version)
)
# Add packages with custom index to tool.uv.sources
if packages_with_index:
for package, index_info in packages_with_index.items():
index_info["index"] = index_info["index"].replace("-", "_")
pyproject_data["tool"]["uv"]["sources"][package] = index_info
# moving dev-dependencies to tool.uv
pyproject_data["tool"]["uv"]["dev-dependencies"] = pyproject_data["project"].pop(
"dev-dependencies"
)
pyproject_data_str = toml.dumps(pyproject_data, encoder=toml.TomlEncoder())
pyproject_data_str = transform_pyproject_data(pyproject_data_str)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as output_file:
output_file.write(pyproject_data_str)
print(f"[green]pyproject.toml generated at '{output_path}'[/green]")
def transform_pyproject_data(input_str: str) -> str:
"""
Transforms the input string containing pyproject data by modifying sections
that match the pattern "[tool.uv.sources.<something>]" and lines that start
with "index =".
Specifically, it performs the following transformations:
1. For lines that match the pattern "[tool.uv.sources.<something>]", it changes
them to "[tool.uv.sources]".
2. For lines within the section that start with "index =", it extracts the value
within the quotes and formats it as "<something> = {index = "<value>"}".
Args:
input_str (str): The input string containing pyproject data.
Returns:
str: The transformed pyproject data as a string.
"""
lines = input_str.split("\n")
transformed_lines = []
current_section = None
for line in lines:
# Check if the line starts with the pattern "[tool.uv.sources."
match = re.match(r"^\[tool\.uv\.sources\.(.*?)\]$", line.strip())
if match:
current_section = match.group(1)
transformed_lines.append("[tool.uv.sources]")
# Check if the line has the format "index = "<somethingB>""
elif current_section and line.strip().startswith("index ="):
index_value = re.search(r'"(.+?)"', line.strip()).group(1)
transformed_lines.append(f'{current_section} = {{index = "{index_value}"}}')
current_section = None
else:
transformed_lines.append(line.rstrip())
return "\n".join(transformed_lines)
def get_input_with_default(prompt, default):
user_input = Prompt.ask(prompt, default=default)
return user_input if user_input else default
def main():
print("[cyan]Welcome to the Pipfile to pyproject.toml converter![/cyan]")
print("[cyan]Please provide the following information:[/cyan]")
pipfile_path = None
while not pipfile_path:
pipfile_path = get_input_with_default(
"Enter the path to your Pipfile: ", "Pipfile"
)
pipfile_path = Path(pipfile_path).resolve()
if not pipfile_path.exists():
print(f"[red]File not found: {pipfile_path}[/red]")
pipfile_path = None
output_path = get_input_with_default(
"Enter the output path for pyproject.toml", "pyproject.toml"
)
output_path = Path(output_path).resolve()
if output_path.exists():
print(
f"[red]CRITICAL WARNING: pyproject.toml file already exists at {output_path}[/red]"
)
if not Confirm.ask("Do you want to proceed?", default=True):
print("[red]Exiting...[/red]")
exit(1)
# try find project name
# 1. Pipfile parent directory name
project_name = pipfile_path.parent.name
project_name = get_input_with_default("Enter the project name", project_name)
project_version = get_input_with_default("Enter the project version", "0.1.0")
project_description = get_input_with_default("Enter the project description", "")
python_version = get_input_with_default(
"Enter the required Python version", "pipfile"
)
convert_pipfile_to_pyproject(
pipfile_path,
output_path,
project_name,
project_version,
project_description,
python_version,
)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment