- New Features - Added `get_full_app_config_path` method to generate full AppConfig paths. - Enhancements - Centralized AppConfig path calculation logic using existing `calculate_app_module_path`. - Bug Fixes - None
353 lines
13 KiB
Python
353 lines
13 KiB
Python
"""
|
|
Shared utility functions.
|
|
Contains common operations used across multiple managers.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from contextlib import contextmanager
|
|
|
|
from djinit.core.base import BaseUtils
|
|
from djinit.templater import template_engine
|
|
from djinit.ui.console import UIFormatter
|
|
|
|
|
|
class CommonUtils(BaseUtils):
|
|
@staticmethod
|
|
def format_file(filename: str) -> None:
|
|
"""Format Python file using Ruff formatter."""
|
|
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
|
|
|
|
@staticmethod
|
|
def create_file_with_content(
|
|
filename: str, content: str, success_message: str, should_format: bool = False
|
|
) -> None:
|
|
from djinit.utils.exceptions import FileError
|
|
|
|
try:
|
|
with open(filename, "w") as file:
|
|
file.write(content)
|
|
except OSError as e:
|
|
raise FileError(f"Failed to write file: {filename}", details=str(e)) from e
|
|
|
|
if should_format:
|
|
CommonUtils.format_file(filename)
|
|
|
|
UIFormatter.print_success(success_message)
|
|
|
|
@staticmethod
|
|
def create_file_from_template(
|
|
file_path: str,
|
|
template_path: str,
|
|
context: dict,
|
|
success_message: str,
|
|
should_format: bool = True,
|
|
) -> None:
|
|
try:
|
|
content = template_engine.render_template(template_path, context)
|
|
except Exception as e:
|
|
from djinit.utils.exceptions import TemplateError
|
|
|
|
raise TemplateError(f"Failed to render template: {template_path}", details=str(e)) from e
|
|
|
|
CommonUtils.create_file_with_content(file_path, content, success_message, should_format=should_format)
|
|
|
|
@staticmethod
|
|
def create_init_file(directory: str, success_message: str) -> None:
|
|
init_path = os.path.join(directory, "__init__.py")
|
|
CommonUtils.create_file_from_template(
|
|
init_path, "project/init.py-tpl", {}, success_message, should_format=False
|
|
)
|
|
|
|
@staticmethod
|
|
def create_directory_with_init(dir_path: str, message: str) -> None:
|
|
"""Create directory and __init__.py file."""
|
|
os.makedirs(dir_path, exist_ok=True)
|
|
CommonUtils.create_init_file(dir_path, message)
|
|
|
|
@staticmethod
|
|
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
|
|
"""Create multiple files from templates.
|
|
|
|
Args:
|
|
base_dir: Base directory where files will be created
|
|
files: List of tuples (filename, template_path, context)
|
|
prefix: Optional prefix for success message
|
|
should_format: Whether to format the created files
|
|
"""
|
|
for file_spec in files:
|
|
if len(file_spec) == 3:
|
|
filename, template_path, context = file_spec
|
|
else:
|
|
# Support (filename, template_path) format with empty context
|
|
filename, template_path = file_spec
|
|
context = {}
|
|
filepath = os.path.join(base_dir, filename)
|
|
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
|
|
CommonUtils.create_file_from_template(
|
|
filepath, template_path, context, message, should_format=should_format
|
|
)
|
|
|
|
@staticmethod
|
|
def get_package_name(project_dir: str) -> str:
|
|
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
|
|
return "backend" if project_dir == "." or not project_dir else project_dir
|
|
|
|
@staticmethod
|
|
@contextmanager
|
|
def change_cwd(path: str):
|
|
original_cwd = os.getcwd()
|
|
os.chdir(path)
|
|
try:
|
|
yield
|
|
finally:
|
|
os.chdir(original_cwd)
|
|
|
|
@staticmethod
|
|
def _parse_app_entry_from_line(line: str) -> str | None:
|
|
"""Extract app name from a line in USER_DEFINED_APPS section."""
|
|
line_stripped = line.strip().strip(",").strip()
|
|
for quote in ('"', "'"):
|
|
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
|
|
return line_stripped.strip(quote)
|
|
return None
|
|
|
|
@staticmethod
|
|
def _is_user_apps_section_start(line: str) -> bool:
|
|
"""Check if line starts USER_DEFINED_APPS section."""
|
|
return "USER_DEFINED_APPS" in line and "=" in line
|
|
|
|
@staticmethod
|
|
def _iterate_user_apps_lines(content: str):
|
|
"""Generator that yields lines within USER_DEFINED_APPS section."""
|
|
lines = content.split("\n")
|
|
in_user_apps = False
|
|
|
|
for line in lines:
|
|
if CommonUtils._is_user_apps_section_start(line):
|
|
in_user_apps = True
|
|
elif in_user_apps and line.strip() == "]":
|
|
break
|
|
elif in_user_apps:
|
|
yield line
|
|
|
|
@staticmethod
|
|
def extract_existing_apps(content: str) -> set:
|
|
existing_apps = set()
|
|
for line in CommonUtils._iterate_user_apps_lines(content):
|
|
app_name = CommonUtils._parse_app_entry_from_line(line)
|
|
if app_name:
|
|
existing_apps.add(app_name)
|
|
return existing_apps
|
|
|
|
@staticmethod
|
|
def format_app_entries(apps: list) -> list:
|
|
return [f' "{app}",' for app in apps]
|
|
|
|
@staticmethod
|
|
def is_app_section_start(line: str) -> bool:
|
|
return "=" in line and any(
|
|
keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS")
|
|
)
|
|
|
|
@staticmethod
|
|
def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool:
|
|
for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))):
|
|
next_line = lines[next_idx].strip()
|
|
if next_line and "BUILT_IN_APPS" in next_line:
|
|
return True
|
|
return True # Default to True if we're inside USER_DEFINED_APPS
|
|
|
|
@staticmethod
|
|
def split_bracket_line(line: str, apps_to_add: list) -> list:
|
|
bracket_pos = line.rfind("]")
|
|
before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip()
|
|
after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]"
|
|
|
|
result = [before_bracket]
|
|
result.extend(CommonUtils.format_app_entries(apps_to_add))
|
|
result.append(after_bracket)
|
|
return result
|
|
|
|
@staticmethod
|
|
def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
|
|
lines = content.split("\n")
|
|
new_lines = []
|
|
in_user_apps = False
|
|
apps_added = False
|
|
|
|
for i, line in enumerate(lines):
|
|
if CommonUtils._is_user_apps_section_start(line) and not apps_added:
|
|
in_user_apps = True
|
|
|
|
if "]" in line:
|
|
new_lines.extend(CommonUtils.split_bracket_line(line, apps_to_add))
|
|
apps_added = True
|
|
in_user_apps = False
|
|
else:
|
|
new_lines.append(line)
|
|
|
|
elif in_user_apps and line.strip() == "]" and not apps_added:
|
|
if CommonUtils.is_user_apps_closing_bracket(lines, i):
|
|
new_lines.extend(CommonUtils.format_app_entries(apps_to_add))
|
|
new_lines.append(line)
|
|
apps_added = True
|
|
in_user_apps = False
|
|
else:
|
|
new_lines.append(line)
|
|
|
|
elif in_user_apps and CommonUtils.is_app_section_start(line) and not apps_added:
|
|
new_lines.extend(CommonUtils.format_app_entries(apps_to_add))
|
|
new_lines.append("]")
|
|
new_lines.append(line)
|
|
apps_added = True
|
|
in_user_apps = False
|
|
|
|
else:
|
|
new_lines.append(line)
|
|
|
|
if not apps_added:
|
|
UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py")
|
|
return None
|
|
|
|
return "\n".join(new_lines)
|
|
|
|
@staticmethod
|
|
def replace_app_in_user_defined_apps(content: str, old_app: str, new_app: str) -> str | None:
|
|
"""Replace an app entry in USER_DEFINED_APPS with a new one."""
|
|
lines = content.split("\n")
|
|
new_lines = []
|
|
replaced = False
|
|
|
|
for line in lines:
|
|
if not replaced and CommonUtils._parse_app_entry_from_line(line) == old_app:
|
|
# Try to preserve indentation
|
|
indent_idx = 0
|
|
for char in line:
|
|
if char.isspace():
|
|
indent_idx += 1
|
|
else:
|
|
break
|
|
indent = line[:indent_idx] if indent_idx > 0 else " "
|
|
|
|
new_lines.append(f'{indent}"{new_app}",')
|
|
replaced = True
|
|
else:
|
|
new_lines.append(line)
|
|
|
|
if replaced:
|
|
return "\n".join(new_lines)
|
|
return None
|
|
|
|
@staticmethod
|
|
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
|
|
nested = bool(metadata.get("nested_apps"))
|
|
nested_dir_name = metadata.get("nested_dir") if nested else None
|
|
return [CommonUtils.calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
|
|
|
|
@staticmethod
|
|
def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
|
|
return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name
|
|
|
|
@staticmethod
|
|
def get_full_app_config_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
|
|
"""Get the full path to the AppConfig class.
|
|
Example: users -> users.apps.UsersConfig
|
|
Example: (nested) apps.users -> apps.users.apps.UsersConfig
|
|
"""
|
|
module_path = CommonUtils.calculate_app_module_path(app_name, nested, nested_dir)
|
|
# Handle cases where app_name might be a module path already
|
|
short_name = app_name.split(".")[-1]
|
|
config_name = short_name.title().replace("_", "") + "Config"
|
|
return f"{module_path}.apps.{config_name}"
|
|
|
|
@staticmethod
|
|
def is_django_project(directory: str = None) -> bool:
|
|
if directory is None:
|
|
directory = os.getcwd()
|
|
manage_py_path = os.path.join(directory, "manage.py")
|
|
return os.path.exists(manage_py_path)
|
|
|
|
@staticmethod
|
|
def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]:
|
|
if search_dir is None:
|
|
search_dir = os.getcwd()
|
|
|
|
for item in os.listdir(search_dir):
|
|
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
|
candidate = os.path.join(search_dir, item)
|
|
base_py = os.path.join(candidate, "settings", "base.py")
|
|
if os.path.exists(base_py):
|
|
return candidate, base_py
|
|
return None, None
|
|
|
|
@staticmethod
|
|
def find_settings_path(search_dir: str = None) -> str | None:
|
|
project_dir, settings_base_path = CommonUtils.find_project_dir(search_dir)
|
|
if settings_base_path:
|
|
return os.path.dirname(settings_base_path)
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_base_settings_path(project_root: str, project_name: str) -> str:
|
|
settings_dir = os.path.join(project_root, project_name, "settings")
|
|
return os.path.join(settings_dir, "base.py")
|
|
|
|
@staticmethod
|
|
def read_base_settings(project_root: str, project_name: str) -> str | None:
|
|
base_settings_path = CommonUtils.get_base_settings_path(project_root, project_name)
|
|
if not os.path.exists(base_settings_path):
|
|
return None
|
|
|
|
with open(base_settings_path) as f:
|
|
return f.read()
|
|
|
|
@staticmethod
|
|
def detect_nested_structure_from_settings(
|
|
base_settings_path: str, search_dir: str = None
|
|
) -> tuple[bool, str | None, str]:
|
|
if search_dir is None:
|
|
search_dir = os.getcwd()
|
|
|
|
if not os.path.exists(base_settings_path):
|
|
return False, None, search_dir
|
|
|
|
try:
|
|
with open(base_settings_path) as f:
|
|
base_content = f.read()
|
|
|
|
for line in CommonUtils._iterate_user_apps_lines(base_content):
|
|
app_label = CommonUtils._parse_app_entry_from_line(line)
|
|
if app_label and "." in app_label:
|
|
nested_dir_name = app_label.split(".", 1)[0]
|
|
nested_path = os.path.join(search_dir, nested_dir_name)
|
|
if os.path.isdir(nested_path):
|
|
return True, nested_dir_name, nested_path
|
|
except Exception:
|
|
pass
|
|
|
|
return False, None, search_dir
|
|
|
|
@staticmethod
|
|
def get_djinit_config(project_root: str = None) -> dict | None:
|
|
"""Read .djinit configuration file."""
|
|
import json
|
|
|
|
if project_root is None:
|
|
project_root = os.getcwd()
|
|
|
|
config_path = os.path.join(project_root, ".djinit")
|
|
if not os.path.exists(config_path):
|
|
# Try finding it in parent directories
|
|
parent = os.path.dirname(project_root)
|
|
if parent and parent != project_root:
|
|
return CommonUtils.get_djinit_config(parent)
|
|
return None
|
|
|
|
try:
|
|
with open(config_path) as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
return None
|