Skip to content

Commit

Permalink
Test the new command
Browse files Browse the repository at this point in the history
  • Loading branch information
epicserve committed Oct 19, 2024
1 parent 13f15e0 commit 9f975ed
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 57 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Test project directory
example_project/
example_project*
test_project*

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down Expand Up @@ -163,3 +164,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
/test_project_6d54gr/
101 changes: 49 additions & 52 deletions src/dj_beat_drop/new.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import os
import re
import secrets
import shutil
from pathlib import Path

from InquirerPy import inquirer

from dj_beat_drop.utils import (
get_latest_django_version,
get_lts_django_version,
green,
red,
)


def get_secret_key():
"""
Return a 50 character random string usable as a SECRET_KEY setting value.
"""
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
return "".join(secrets.choice(chars) for _ in range(50))
from dj_beat_drop import utils
from dj_beat_drop.utils import color


def rename_template_files(project_dir):
Expand Down Expand Up @@ -85,48 +73,19 @@ def replace_variables(project_dir, context: dict[str, str], initialize_env):
f.write(content)


def handle_new(name, use_lts, overwrite_target_dir):
if name is None:
name = inquirer.text("Project name:").execute()

if re.match(r"^[-a-z_]+$", name) is None:
red("Invalid project name. Please use only lowercase letters, hyphens, and underscores.")
return

django_version, minor_version = get_latest_django_version()
if use_lts is True:
django_version, minor_version = get_lts_django_version()
project_dir = os.path.join(os.getcwd(), name)
def create_new_project(
*, name: str, use_lts: bool, project_dir: Path, initialize_uv: bool, initialize_env: bool
) -> dict[str, str]:
template_context = utils.get_template_context(use_lts=use_lts)
minor_version = template_context["docs_version"]
template_dir_src = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates", minor_version)

if os.path.exists(project_dir):
if overwrite_target_dir is False:
overwrite_response = inquirer.confirm(
message=f"The directory '{name}' already exists. Do you want to overwrite it?",
default=True,
).execute()
if overwrite_response is False:
red("Operation cancelled.")
return
shutil.rmtree(project_dir)

initialize_uv = inquirer.confirm(message="Initialize your project with UV?", default=True).execute()
initialize_env = inquirer.confirm(
message="Initialize your project with an .env file and environs?", default=True
).execute()

shutil.copytree(str(template_dir_src), project_dir)
os.rename(os.path.join(project_dir, "project_name"), os.path.join(project_dir, "config"))

rename_template_files(project_dir)
replace_variables(
project_dir,
{
"project_name": "config",
"django_version": django_version,
"docs_version": minor_version,
"secret_key": get_secret_key(),
},
template_context,
initialize_env,
)

Expand All @@ -139,9 +98,47 @@ def handle_new(name, use_lts, overwrite_target_dir):
os.system("uv add environs[django]")
os.system("uv run manage.py migrate")

green("New Django project created.\n")
color.green("New Django project created.\n")

if initialize_uv is True:
green("To start Django's run server:\n")
color.green("To start Django's run server:\n")
print(f"cd {name}")
print("uv run manage.py runserver")

return template_context


def handle_new(name: str, use_lts: bool, overwrite_target_dir: bool) -> None:
if name is None:
name = inquirer.text("Project name:").execute()

if re.match(r"^[-a-z_]+$", name) is None:
color.red("Invalid project name. Please use only lowercase letters, hyphens, and underscores.")
return
project_dir = os.path.join(os.getcwd(), name)

if os.path.exists(project_dir):
if overwrite_target_dir is False:
overwrite_response = inquirer.confirm(
message=f"The directory '{name}' already exists. Do you want to overwrite it?",
default=True,
).execute()
if overwrite_response is False:
color.red("Operation cancelled.")
return
shutil.rmtree(project_dir)

initialize_uv = inquirer.confirm(message="Initialize your project with UV?", default=True).execute()
initialize_env = inquirer.confirm(
message="Initialize your project with an .env file and environs?", default=True
).execute()

create_new_project(
**{
"name": name,
"use_lts": use_lts,
"project_dir": project_dir,
"initialize_uv": initialize_uv,
"initialize_env": initialize_env,
}
)
34 changes: 30 additions & 4 deletions src/dj_beat_drop/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import secrets
from functools import lru_cache

import requests


def green(text):
print(f"\033[92m{text}\033[0m")
class Color:
@staticmethod
def green(text):
print(f"\033[92m{text}\033[0m")

@staticmethod
def red(text):
print(f"\033[91m{text}\033[0m")

def red(text):
print(f"\033[91m{text}\033[0m")

color = Color()


@lru_cache
Expand Down Expand Up @@ -42,3 +48,23 @@ def get_lts_django_version():
version_parts = release.split(".")
if version_parts[1] == "2":
return release, ".".join(version_parts[0:2])


def get_template_context(*, use_lts: bool):
django_version, minor_version = get_latest_django_version()
if use_lts is True:
django_version, minor_version = get_lts_django_version()
return {
"project_name": "config",
"django_version": django_version,
"docs_version": minor_version,
"secret_key": get_secret_key(),
}


def get_secret_key():
"""
Return a 50 character random string usable as a SECRET_KEY setting value.
"""
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
return "".join(secrets.choice(chars) for _ in range(50))
193 changes: 193 additions & 0 deletions tests/test_new_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import random
import re
import shutil
import string
import sys
from pathlib import Path
from unittest import TestCase

from dj_beat_drop.new import create_new_project


ENV_SECRET_KEY_PATTERN = 'SECRET_KEY = env.str("SECRET_KEY")'
FILE_ASSERTIONS = {
"manage.py": [
"os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings')",
],
"config/asgi.py": [
"ASGI config for {{ project_name }} project.",
"/{{ docs_version }}/howto/deployment/asgi/",
"os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings')",
],
"config/settings.py": [
"Django settings for {{ project_name }} project.",
"/en/{{ docs_version }}/topics/settings/",
"/en/{{ docs_version }}/howto/deployment/checklist/",
"SECRET_KEY = '{{ secret_key }}'",
"ROOT_URLCONF = '{{ project_name }}.urls'",
"WSGI_APPLICATION = '{{ project_name }}.wsgi.application'",
],
"config/urls.py": [
"/en/{{ docs_version }}/topics/http/urls/",
],
"config/wsgi.py": [
"/en/{{ docs_version }}/howto/deployment/wsgi/",
"os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings')",
],
}
UV_ASSERTIONS = {
".python-version": [
"{{ python_version }}",
],
"pyproject.toml": [
'name = "{{ project_dir_name }}"',
"django~={{ docs_version }}",
],
"README.md": [],
}
ENV_ASSERTIONS = {
".env": [
"DEBUG=True",
'SECRET_KEY="{{ secret_key }}"',
"ALLOWED_HOSTS=",
"DATABASE_URL=sqlite:///{{ project_dir }}/db.sqlite3",
],
"config/settings.py": [
"from environs import Env",
ENV_SECRET_KEY_PATTERN,
'DEBUG = env.bool("DEBUG")',
'ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")',
'DATABASES = {"default": env.dj_db_url("DATABASE_URL")}',
],
}


class SafeDict(dict):
def __missing__(self, key):
return f"{{{key}}}" # Keeps the original placeholder if the key is missing


class TestNewCommand(TestCase):
@staticmethod
def _generate_random_hash(length=6):
characters = string.ascii_lowercase + string.digits
return "".join(random.choice(characters) for _ in range(length)) # noqa: S311

def setUp(self):
random_hash = self._generate_random_hash()
self.project_name = f"test_project_{random_hash}"
self.project_dir = Path(__file__).parent.parent / self.project_name
self.new_project_kwargs: dict[str, str | dict | bool | Path] = {
"name": self.project_name,
"use_lts": True,
"project_dir": self.project_dir,
"initialize_uv": True,
"initialize_env": True,
}
if self.project_dir.exists():
shutil.rmtree(self.project_dir)

def tearDown(self):
if self.project_dir.exists():
shutil.rmtree(self.project_dir)

@staticmethod
def assert_files_are_correct(
*,
name: str,
use_lts: bool,
project_dir: Path,
initialize_uv: bool,
initialize_env: bool,
template_context: dict,
):
assertion_context = template_context.copy()
assertion_context["project_dir_name"] = project_dir.name.replace("_", "-")
assertion_context["project_dir"] = str(project_dir)
assertion_context["python_version"] = f"{sys.version_info.major}.{sys.version_info.minor}"
for file in project_dir.rglob("*"):
relative_path = str(file.relative_to(project_dir))
assertions = []
if relative_path in FILE_ASSERTIONS or relative_path in UV_ASSERTIONS or relative_path in ENV_ASSERTIONS:
assertions.extend(FILE_ASSERTIONS.get(relative_path, []))
uv_assertions = UV_ASSERTIONS.get(relative_path, [])
env_assertions = ENV_ASSERTIONS.get(relative_path, [])
if initialize_uv is True:
assertions.extend(uv_assertions)
if initialize_env is True:
assertions.extend(env_assertions)
with open(file) as f:
content = f.read()
for assertion_pattern in assertions:
if (
assertion_pattern.startswith("SECRET_KEY =")
and initialize_env is True
and relative_path == "config/settings.py"
):
assertion_pattern = ENV_SECRET_KEY_PATTERN
if re.match(r".*{{\s[_a-z]+\s}}.*", assertion_pattern) is None:
assertion = assertion_pattern
else:
formatted_assertion = assertion_pattern.replace("{{ ", "{").replace(" }}", "}")
assertion = formatted_assertion.format_map(SafeDict(assertion_context))
assert assertion in content, f"Assertion failed for {relative_path}: {assertion}"

def test_new_command_with_defaults(self):
kwargs = self.new_project_kwargs.copy()
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

def test_new_command_with_no_env(self):
kwargs = self.new_project_kwargs.copy()
kwargs["initialize_env"] = False
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

def test_new_command_with_no_uv(self):
kwargs = self.new_project_kwargs.copy()
kwargs["initialize_uv"] = False
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

def test_new_command_with_no_uv_and_no_env(self):
kwargs = self.new_project_kwargs.copy()
kwargs["initialize_uv"] = False
kwargs["initialize_env"] = False
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

def test_new_command_with_defaults_and_latest_dj(self):
kwargs = self.new_project_kwargs.copy()
kwargs["use_lts"] = False
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

def test_new_command_with_no_env_and_latest_dj(self):
kwargs = self.new_project_kwargs.copy()
kwargs["use_lts"] = False
kwargs["initialize_env"] = False
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

def test_new_command_with_no_uv_and_latest_dj(self):
kwargs = self.new_project_kwargs.copy()
kwargs["use_lts"] = False
kwargs["initialize_uv"] = False
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

def test_new_command_with_no_uv_and_no_env_and_latest_dj(self):
kwargs = self.new_project_kwargs.copy()
kwargs["use_lts"] = False
kwargs["initialize_uv"] = False
kwargs["initialize_env"] = False
template_context = create_new_project(**kwargs)
kwargs["template_context"] = template_context
self.assert_files_are_correct(**kwargs)

0 comments on commit 9f975ed

Please sign in to comment.