refactor: improve project directory handling and subprocess execution for compatibility
This commit is contained in:
6 files changed
+196
-39
No files matched your search
+5
-1
@@ -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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -44,6 +44,7 @@ dependencies = [
|
||||
"rich>=14.2.0",
|
||||
"ruff>=0.14.2",
|
||||
"jinja2>=3.1.0",
|
||||
"django>=4.2,<6.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
Reference in new issue
Block a user