| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- # 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
- pyside_table = root_table.get("tool", {}).get("pyside6-project", {})
- if not pyside_table:
- result.errors.append("Missing [tool.pyside6-project] table")
- return result
- 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
|