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