refactor: improve project directory handling and subprocess execution for compatibility

This commit is contained in:
sankalp committed 2025-11-02 13:41:30 +05:45
1 parent 4369cdd44f
commit 462251059c
6 files changed
+196 -39

No files matched your search

+5 -1
View File
@@ -22,7 +22,11 @@ class Cli:
self.primary_app = primary_app
self.app_names = app_names
self.metadata = metadata
self.project_root = os.path.join(os.getcwd(), project_dir)
# Handle '.' for current directory
if project_dir == ".":
self.project_root = os.getcwd()
else:
self.project_root = os.path.join(os.getcwd(), project_dir)
# Initialize managers
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
+3 -1
View File
@@ -5,6 +5,7 @@ Handles creation of Django apps and updating settings.
import os
import subprocess
import sys
from typing import Optional
from djinit.scripts.console_ui import UIFormatter
@@ -61,8 +62,9 @@ class AppManager:
return False
# Create app in the correct location
# Use 'python -m django' instead of 'django-admin' for better compatibility
with change_cwd(apps_base_dir):
subprocess.run(["django-admin", "startapp", self.app_name], capture_output=True, text=True, check=True)
subprocess.run([sys.executable, "-m", "django", "startapp", self.app_name], capture_output=True, text=True, check=True)
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
return True
+177 -34
View File
@@ -183,11 +183,14 @@ class InputCollector:
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]", end="")
sys.stdout.flush()
choice = self.char_reader.get_char()
console.print(choice.upper())
# Get valid keys from CICDOption enum
valid_keys = [option.key for option in CICDOption]
choice = CharReader.get_cicd_choice(
f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]",
valid_keys=valid_keys,
default=CICDOption.NONE.key
)
# Parse choice
if choice == CICDOption.BOTH.key:
@@ -196,7 +199,7 @@ class InputCollector:
return True, False
elif choice == CICDOption.GITLAB.key:
return False, True
else: # Default to none for any unrecognized input
else: # Default to none (should not happen due to validation, but kept as fallback)
return False, False
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
@@ -211,10 +214,10 @@ class InputCollector:
)
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}] ", end="")
sys.stdout.flush()
choice = self.char_reader.get_char()
console.print(choice.upper())
choice = CharReader.get_yes_no(
f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]",
default="n"
)
if choice != "y":
return False, None
@@ -240,13 +243,13 @@ class InputCollector:
console.print(f"[{UIColors.MUTED}]postgres://user:password@host:port/database[/{UIColors.MUTED}]")
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}] ", end="")
sys.stdout.flush()
choice = self.char_reader.get_char()
console.print(choice.upper())
choice = CharReader.get_yes_no(
f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]",
default="y"
)
# Default to True (use DATABASE_URL) if user presses Enter or Y
return choice.lower() != "n"
# Return True if choice is 'y', False if 'n'
return choice == "y"
class CharReader:
@@ -258,6 +261,117 @@ class CharReader:
return CharReader._get_char_windows()
return CharReader._get_char_unix()
@staticmethod
def get_yes_no(prompt: str, default: str = "n") -> str:
max_attempts = 3
attempt = 0
while attempt < max_attempts:
try:
console.print(f"{prompt} ", end="")
sys.stdout.flush()
reader = CharReader()
response = reader.get_char()
# Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return)
# Also handle empty string or other control characters as Enter
if not response or response == '\r' or response == '\n' or ord(response) < 32:
console.print(default.upper())
return default.lower()
response = response.lower()
# Validate response
if response in ('y', 'n'):
console.print(response.upper())
return response
# Invalid input - show error (only show if it's a printable character)
console.print()
if response.isprintable():
UIFormatter.print_error(f"Invalid input '{response}'. Please enter 'y' or 'n'.")
attempt += 1
if attempt < max_attempts:
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]")
console.print()
else:
# Control character entered, treat as Enter
console.print(default.upper())
return default.lower()
except KeyboardInterrupt:
console.print()
UIFormatter.print_info("\nOperation cancelled by user.")
raise
# Max attempts reached - use default
console.print()
UIFormatter.print_warning(f"Maximum attempts reached. Using default: '{default.upper()}'.")
return default.lower()
@staticmethod
def get_cicd_choice(prompt: str, valid_keys: list[str], default: str = "n") -> str:
"""
Get CI/CD choice input with validation and retries.
Args:
prompt: The prompt message to display
valid_keys: List of valid key characters (e.g., ['g', 'l', 'b', 'n'])
default: Default value if Enter is pressed
Returns:
Valid key (lowercase)
"""
max_attempts = 3
attempt = 0
valid_keys_lower = [k.lower() for k in valid_keys]
while attempt < max_attempts:
try:
console.print(f"{prompt} ", end="")
sys.stdout.flush()
reader = CharReader()
response = reader.get_char()
# Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return)
# Also handle empty string or other control characters as Enter
if not response or response == '\r' or response == '\n' or ord(response) < 32:
console.print(default.upper())
return default.lower()
response = response.lower()
# Validate response
if response in valid_keys_lower:
console.print(response.upper())
return response
# Invalid input - show error (only show if it's a printable character)
console.print()
if response.isprintable():
valid_keys_str = ", ".join(f"'{k}'" for k in valid_keys)
UIFormatter.print_error(f"Invalid input '{response}'. Please enter one of: {valid_keys_str}.")
attempt += 1
if attempt < max_attempts:
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]")
console.print()
else:
# Control character entered, treat as Enter
console.print(default.upper())
return default.lower()
except KeyboardInterrupt:
console.print()
UIFormatter.print_info("\nOperation cancelled by user.")
raise
# Max attempts reached - use default
console.print()
UIFormatter.print_warning(f"Maximum attempts reached. Using default: '{default.upper()}'.")
return default.lower()
@staticmethod
def _get_char_windows() -> str:
import msvcrt
@@ -299,17 +413,49 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory[/{UIColors.HIGHLIGHT}]")
console.print(f"[{UIColors.MUTED}]Where your project files will be created[/{UIColors.MUTED}]")
project_dir = collector.get_validated_input(
"Enter project directory name", validate_project_name, "project directory name"
)
# Check if directory exists and ask for alternative
while os.path.exists(project_dir):
UIFormatter.print_error(f"Directory '{project_dir}' already exists.")
UIFormatter.print_info("Please choose a different project directory name.")
project_dir = collector.get_validated_input(
"Enter project directory name", validate_project_name, "project directory name"
)
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
# Get project directory with special handling for '.' and empty input
attempt = 0
project_dir = None
while attempt < collector.MAX_ATTEMPTS and project_dir is None:
try:
user_input = console.input(f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] ").strip()
# Handle empty input or '.' - use current directory
if not user_input or user_input == ".":
project_dir = "."
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
break
# Validate the input
is_valid, error_msg = validate_project_name(user_input)
if not is_valid:
UIFormatter.print_error(error_msg)
attempt += 1
collector._show_retry_message(attempt)
continue
# Check if directory exists and ask for alternative
if os.path.exists(user_input):
UIFormatter.print_error(f"Directory '{user_input}' already exists.")
UIFormatter.print_info("Please choose a different project directory name (or press Enter for current directory).")
attempt += 1
collector._show_retry_message(attempt)
continue
project_dir = user_input
break
except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.")
sys.exit(0)
if project_dir is None:
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
sys.exit(1)
console.print()
console.print(f"[{UIColors.HIGHLIGHT}]Django Project Name[/{UIColors.HIGHLIGHT}]")
@@ -377,13 +523,10 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
# Get confirmation
try:
console.print(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}] ", end="")
sys.stdout.flush()
reader = CharReader()
response = reader.get_char()
console.print(response.upper())
response = CharReader.get_yes_no(
f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]",
default="n"
)
return response == "y"
except KeyboardInterrupt:
+7 -2
View File
@@ -18,7 +18,11 @@ class ProjectManager:
self.project_name = project_name
self.app_names = app_names
self.metadata = metadata
self.project_root = os.path.join(os.getcwd(), project_dir)
# Handle '.' for current directory
if project_dir == ".":
self.project_root = os.getcwd()
else:
self.project_root = os.path.join(os.getcwd(), project_dir)
def create_project(self) -> bool:
self._ensure_django_installed()
@@ -27,8 +31,9 @@ class ProjectManager:
os.makedirs(self.project_root, exist_ok=True)
# Create Django project inside the directory
# Use 'python -m django' instead of 'django-admin' for better compatibility
subprocess.run(
["django-admin", "startproject", self.project_name, self.project_root],
[sys.executable, "-m", "django", "startproject", self.project_name, self.project_root],
check=True,
)
+3 -1
View File
@@ -5,6 +5,7 @@ Contains common operations used across multiple managers.
import os
import subprocess
import sys
from contextlib import contextmanager
from djinit.scripts.console_ui import UIFormatter
@@ -12,7 +13,8 @@ from djinit.scripts.console_ui import UIFormatter
def format_file(filename: str) -> None:
"""Format Python file using Ruff formatter."""
subprocess.run(["ruff", "format", filename], check=False, capture_output=True)
# Use 'python -m ruff' instead of 'ruff' for better compatibility
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> bool:
+1
View File
@@ -44,6 +44,7 @@ dependencies = [
"rich>=14.2.0",
"ruff>=0.14.2",
"jinja2>=3.1.0",
"django>=4.2,<6.0",
]
[project.urls]