Initial commit
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
# Copyright (C) 2025 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
# TODO: Remove this import when Python 3.11 is the minimum supported version
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from . import PYPROJECT_JSON_PATTERN
|
||||
from .pyproject_parse_result import PyProjectParseResult
|
||||
from .pyproject_json import parse_pyproject_json
|
||||
|
||||
|
||||
def _parse_toml_content(content: str) -> dict:
|
||||
"""
|
||||
Parse TOML content for project name and files list only.
|
||||
"""
|
||||
result = {"project": {}, "tool": {"pyside6-project": {}}}
|
||||
current_section = None
|
||||
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
if line == '[project]':
|
||||
current_section = 'project'
|
||||
elif line == '[tool.pyside6-project]':
|
||||
current_section = 'tool.pyside6-project'
|
||||
elif '=' in line and current_section:
|
||||
key, value = [part.strip() for part in line.split('=', 1)]
|
||||
|
||||
# Handle string values - name of the project
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
# Handle array of strings - files names
|
||||
elif value.startswith('[') and value.endswith(']'):
|
||||
items = value[1:-1].split(',')
|
||||
value = [item.strip().strip('"') for item in items if item.strip()]
|
||||
|
||||
if current_section == 'project':
|
||||
result['project'][key] = value
|
||||
else: # tool.pyside6-project
|
||||
result['tool']['pyside6-project'][key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _write_base_toml_content(data: dict) -> str:
|
||||
"""
|
||||
Write minimal TOML content with project and tool.pyside6-project sections.
|
||||
"""
|
||||
lines = []
|
||||
|
||||
if data.get('project'):
|
||||
lines.append('[project]')
|
||||
for key, value in sorted(data['project'].items()):
|
||||
if isinstance(value, str):
|
||||
lines.append(f'{key} = "{value}"')
|
||||
|
||||
if data.get("tool") and data['tool'].get('pyside6-project'):
|
||||
lines.append('\n[tool.pyside6-project]')
|
||||
for key, value in sorted(data['tool']['pyside6-project'].items()):
|
||||
if isinstance(value, list):
|
||||
items = [f'"{item}"' for item in sorted(value)]
|
||||
lines.append(f'{key} = [{", ".join(items)}]')
|
||||
else:
|
||||
lines.append(f'{key} = "{value}"')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def parse_pyproject_toml(pyproject_toml_file: Path) -> PyProjectParseResult:
|
||||
"""
|
||||
Parse a pyproject.toml file and return a PyProjectParseResult object.
|
||||
"""
|
||||
result = PyProjectParseResult()
|
||||
|
||||
try:
|
||||
content = pyproject_toml_file.read_text(encoding='utf-8')
|
||||
# TODO: Remove the manual parsing when Python 3.11 is the minimum supported version
|
||||
if sys.version_info >= (3, 11):
|
||||
root_table = tomllib.loads(content) # Use tomllib for Python >= 3.11
|
||||
print("Using tomllib for parsing TOML content")
|
||||
else:
|
||||
root_table = _parse_toml_content(content) # Fallback to manual parsing
|
||||
except Exception as e:
|
||||
result.errors.append(str(e))
|
||||
return result
|
||||
|
||||
tool_entry = root_table.get("tool", {})
|
||||
pyside_table = tool_entry.get("pyside6-project", {})
|
||||
if not pyside_table:
|
||||
result.errors.append("Missing [tool.pyside6-project] table")
|
||||
return result
|
||||
|
||||
if rcc_table := tool_entry.get("pyside6-rcc", {}):
|
||||
result.rcc_options = rcc_table.get("options", [])
|
||||
|
||||
if uic_table := tool_entry.get("pyside6-uic", {}):
|
||||
result.uic_options = uic_table.get("options", [])
|
||||
|
||||
files = pyside_table.get("files", [])
|
||||
if not isinstance(files, list):
|
||||
result.errors.append("Missing or invalid files list")
|
||||
return result
|
||||
|
||||
# Convert paths
|
||||
for file in files:
|
||||
if not isinstance(file, str):
|
||||
result.errors.append(f"Invalid file: {file}")
|
||||
return result
|
||||
file_path = Path(file)
|
||||
if not file_path.is_absolute():
|
||||
file_path = (pyproject_toml_file.parent / file).resolve()
|
||||
result.files.append(file_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def write_pyproject_toml(pyproject_file: Path, project_name: str, project_files: list[str]):
|
||||
"""
|
||||
Create or overwrite a pyproject.toml file with the specified content.
|
||||
"""
|
||||
data = {
|
||||
"project": {"name": project_name},
|
||||
"tool": {
|
||||
"pyside6-project": {"files": sorted(project_files)}
|
||||
}
|
||||
}
|
||||
|
||||
content = _write_base_toml_content(data)
|
||||
try:
|
||||
pyproject_file.write_text(content, encoding='utf-8')
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error writing TOML file: {str(e)}")
|
||||
|
||||
|
||||
def robust_relative_to_posix(target_path: Path, base_path: Path) -> str:
|
||||
"""
|
||||
Calculates the relative path from base_path to target_path.
|
||||
Uses Path.relative_to first, falls back to os.path.relpath if it fails.
|
||||
Returns the result as a POSIX path string.
|
||||
"""
|
||||
# Ensure both paths are absolute for reliable calculation, although in this specific code,
|
||||
# project_folder and paths in output_files are expected to be resolved/absolute already.
|
||||
abs_target = target_path.resolve() if not target_path.is_absolute() else target_path
|
||||
abs_base = base_path.resolve() if not base_path.is_absolute() else base_path
|
||||
|
||||
try:
|
||||
return abs_target.relative_to(abs_base).as_posix()
|
||||
except ValueError:
|
||||
# Fallback to os.path.relpath which is more robust for paths that are not direct subpaths.
|
||||
relative_str = os.path.relpath(str(abs_target), str(abs_base))
|
||||
# Convert back to Path temporarily to get POSIX format
|
||||
return Path(relative_str).as_posix()
|
||||
|
||||
|
||||
def migrate_pyproject(pyproject_file: Path | str = None) -> int:
|
||||
"""
|
||||
Migrate a project *.pyproject JSON file to the new pyproject.toml format.
|
||||
|
||||
The containing subprojects are migrated recursively.
|
||||
|
||||
:return: 0 if successful, 1 if an error occurred.
|
||||
"""
|
||||
project_name = None
|
||||
|
||||
# Transform the user input string into a Path object
|
||||
if isinstance(pyproject_file, str):
|
||||
pyproject_file = Path(pyproject_file)
|
||||
|
||||
if pyproject_file:
|
||||
if not pyproject_file.match(PYPROJECT_JSON_PATTERN):
|
||||
print(f"Cannot migrate non \"{PYPROJECT_JSON_PATTERN}\" file:", file=sys.stderr)
|
||||
print(f"\"{pyproject_file}\"", file=sys.stderr)
|
||||
return 1
|
||||
project_files = [pyproject_file]
|
||||
project_name = pyproject_file.stem
|
||||
else:
|
||||
# Get the existing *.pyproject files in the current directory
|
||||
project_files = list(Path().glob(PYPROJECT_JSON_PATTERN))
|
||||
if not project_files:
|
||||
print(f"No project file found in the current directory: {Path()}", file=sys.stderr)
|
||||
return 1
|
||||
if len(project_files) > 1:
|
||||
print("Multiple pyproject files found in the project folder:")
|
||||
print('\n'.join(str(project_file) for project_file in project_files))
|
||||
response = input("Continue? y/n: ")
|
||||
if response.lower().strip() not in {"yes", "y"}:
|
||||
return 0
|
||||
else:
|
||||
# If there is only one *.pyproject file in the current directory,
|
||||
# use its file name as the project name
|
||||
project_name = project_files[0].stem
|
||||
|
||||
# The project files that will be written to the pyproject.toml file
|
||||
output_files: set[Path] = set()
|
||||
for project_file in project_files:
|
||||
project_data = parse_pyproject_json(project_file)
|
||||
if project_data.errors:
|
||||
print(f"Invalid project file: {project_file}. Errors found:", file=sys.stderr)
|
||||
print('\n'.join(project_data.errors), file=sys.stderr)
|
||||
return 1
|
||||
output_files.update(project_data.files)
|
||||
|
||||
project_folder = project_files[0].parent.resolve()
|
||||
if project_name is None:
|
||||
# If a project name has not resolved, use the name of the parent folder
|
||||
project_name = project_folder.name
|
||||
|
||||
pyproject_toml_file = project_folder / "pyproject.toml"
|
||||
|
||||
relative_files = sorted(
|
||||
robust_relative_to_posix(p, project_folder) for p in output_files
|
||||
)
|
||||
|
||||
if not (already_existing_file := pyproject_toml_file.exists()):
|
||||
# Create new pyproject.toml file
|
||||
data = {
|
||||
"project": {"name": project_name},
|
||||
"tool": {
|
||||
"pyside6-project": {"files": relative_files}
|
||||
}
|
||||
}
|
||||
updated_content = _write_base_toml_content(data)
|
||||
else:
|
||||
# For an already existing file, append our tool.pyside6-project section
|
||||
# If the project section is missing, add it
|
||||
try:
|
||||
content = pyproject_toml_file.read_text(encoding='utf-8')
|
||||
except Exception as e:
|
||||
print(f"Error processing existing TOML file: {str(e)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
append_content = []
|
||||
|
||||
if '[project]' not in content:
|
||||
# Add project section if needed
|
||||
append_content.append('\n[project]')
|
||||
append_content.append(f'name = "{project_name}"')
|
||||
|
||||
if '[tool.pyside6-project]' not in content:
|
||||
# Add tool.pyside6-project section
|
||||
append_content.append('\n[tool.pyside6-project]')
|
||||
items = [f'"{item}"' for item in relative_files]
|
||||
append_content.append(f'files = [{", ".join(items)}]')
|
||||
|
||||
if append_content:
|
||||
updated_content = content.rstrip() + '\n' + '\n'.join(append_content)
|
||||
else:
|
||||
# No changes needed
|
||||
print("pyproject.toml already contains [project] and [tool.pyside6-project] sections")
|
||||
return 0
|
||||
|
||||
print(f"WARNING: A pyproject.toml file already exists at \"{pyproject_toml_file}\"")
|
||||
print("The file will be updated with the following content:")
|
||||
print(updated_content)
|
||||
response = input("Proceed? [Y/n] ")
|
||||
if response.lower().strip() not in {"yes", "y"}:
|
||||
return 0
|
||||
|
||||
try:
|
||||
pyproject_toml_file.write_text(updated_content, encoding='utf-8')
|
||||
except Exception as e:
|
||||
print(f"Error writing to \"{pyproject_toml_file}\": {str(e)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not already_existing_file:
|
||||
print(f"Created \"{pyproject_toml_file}\"")
|
||||
else:
|
||||
print(f"Updated \"{pyproject_toml_file}\"")
|
||||
|
||||
# Recursively migrate the subprojects
|
||||
for sub_project_file in filter(lambda f: f.match(PYPROJECT_JSON_PATTERN), output_files):
|
||||
result = migrate_pyproject(sub_project_file)
|
||||
if result != 0:
|
||||
return result
|
||||
return 0
|
||||
Reference in New Issue
Block a user