-
-
Save andrader/a020beb04f4366eff14aec12fb26d75a to your computer and use it in GitHub Desktop.
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
# /// 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