1
0
mirror of https://github.com/django/django.git synced 2025-03-29 02:30:48 +00:00

Refs #34976 -- Print directory structure after startapp and startproject commands

Print the newly created directory structure after running the startapp and startproject commands.

Refs #34976

Use `make_style`  to style directories

Support --no-color flag in get_directory_tree
This commit is contained in:
TobeTek 2024-02-09 00:09:31 +01:00
parent f515449826
commit f44a91902b
5 changed files with 214 additions and 0 deletions

View File

@ -271,6 +271,7 @@ class BaseCommand:
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
self.stdout = OutputWrapper(stdout or sys.stdout)
self.stderr = OutputWrapper(stderr or sys.stderr)
self.no_color = no_color
if no_color and force_color:
raise CommandError("'no_color' and 'force_color' can't be used together.")
if no_color:
@ -442,6 +443,7 @@ class BaseCommand:
if options["force_color"]:
self.style = color_style(force_color=True)
elif options["no_color"]:
self.no_color = options["no_color"]
self.style = no_style()
self.stderr.style_func = None
if options.get("stdout"):

View File

@ -13,6 +13,7 @@ from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.management.utils import (
find_formatters,
get_directory_tree,
handle_extensions,
run_formatters,
)
@ -230,6 +231,8 @@ class TemplateCommand(BaseCommand):
shutil.rmtree(path_to_remove)
run_formatters([top_dir], **formatter_paths)
for subpath in get_directory_tree(top_dir, no_color=self.no_color):
self.stdout.write(subpath)
def handle_template(self, template, subdir):
"""

View File

@ -6,6 +6,7 @@ from pathlib import Path
from subprocess import run
from django.apps import apps as installed_apps
from django.core.management.color import make_style, no_style, supports_color
from django.utils.crypto import get_random_string
from django.utils.encoding import DEFAULT_LOCALE_ENCODING
@ -173,3 +174,57 @@ def run_formatters(written_files, black_path=(sentinel := object())):
[black_path, "--fast", "--", *written_files],
capture_output=True,
)
def get_directory_tree(path, max_depth=10, prefix="", depth=0, **options):
"""
Get the folder structure of a directory and its content,
akin to the `tree` command.
Adapted from: https://stackoverflow.com/a/59109706
Example:
>>> for line in get_directory_tree(Path.home()):
... print(line)
"""
SPACE_PREFIX = " "
BRANCH_PREFIX = "| "
ELBOW_PREFIX = "|___ "
path = Path(path)
if (supports_color() and not options.get("no_color", False)) or (
options.get("force_color", False)
):
directory_style = make_style("notice=blue,bold;")
else:
directory_style = no_style()
if depth == 0:
yield directory_style.NOTICE(path.name)
if not path.is_dir():
raise CommandError("'%s' is not a directory" % (path))
if depth >= max_depth:
return
dir_contents = list(path.iterdir())
for index, subpath in enumerate(sorted(dir_contents)):
# Make folders appear bolder in terminal
subpath_name = (
directory_style.NOTICE(subpath.name) if subpath.is_dir() else subpath.name
)
is_last_element = index == len(dir_contents) - 1
yield prefix + ELBOW_PREFIX + subpath_name
if subpath.is_dir():
extension = SPACE_PREFIX if is_last_element else BRANCH_PREFIX
yield from get_directory_tree(
subpath,
max_depth,
prefix=prefix + extension,
depth=depth + 1,
**options,
)

View File

@ -2405,6 +2405,31 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
self.assertOutput(err, "usage:")
self.assertOutput(err, "You must provide a project name.")
def test_output_directory_structure(self):
dirs = {
"testproject": self.test_dir,
"testproject1": os.path.join(self.test_dir, "otherdirectory"),
}
for name_project, dir in dirs.items():
with self.subTest(name_project=name_project, dir=dir):
args = ["startproject", name_project]
if not os.path.exists(dir):
os.mkdir(dir)
args.append(dir)
out, err = self.run_django_admin(args)
# Confirm the directory structure is as expected
self.assertIn(name_project, out)
self.assertIn("|___ manage.py", out)
self.assertIn("|___ testproject", out)
self.assertIn(" |___ __init__.py", out)
self.assertIn(" |___ asgi.py", out)
self.assertIn(" |___ settings.py", out)
self.assertIn(" |___ urls.py", out)
self.assertIn(" |___ wsgi.py", out)
self.assertNoOutput(err)
def test_simple_project(self):
"Make sure the startproject management command creates a project"
args = ["startproject", "testproject"]
@ -2879,6 +2904,28 @@ class StartApp(AdminScriptTestCase):
)
self.assertFalse(os.path.exists(testproject_dir))
def test_output_directory_structure(self):
dirs = {
"foo": self.test_dir,
"bar": os.path.join(self.test_dir, "otherdir"),
}
for name_app, dir in dirs.items():
with self.subTest(name_project=name_app, dir=dir):
args = ["startapp", name_app]
out, err = self.run_django_admin(args)
# Confirm the directory structure is as expected
self.assertIn(name_app, out)
self.assertIn("|___ __init__.py", out)
self.assertIn("|___ admin.py", out)
self.assertIn("|___ apps.py", out)
self.assertIn("|___ migrations", out)
self.assertIn("| |___ __init__.py", out)
self.assertIn("|___ models.py", out)
self.assertIn("|___ tests.py", out)
self.assertIn("|___ views.py", out)
self.assertNoOutput(err)
def test_importable_name(self):
"""
startapp validates that app name doesn't clash with existing Python

View File

@ -1,6 +1,8 @@
import os
import tempfile
from argparse import ArgumentDefaultsHelpFormatter
from io import StringIO
from pathlib import Path
from unittest import mock
from admin_scripts.tests import AdminScriptTestCase
@ -11,6 +13,7 @@ from django.core.checks import Tags
from django.core.management import BaseCommand, CommandError, find_commands
from django.core.management.utils import (
find_command,
get_directory_tree,
get_random_secret_key,
is_ignored_path,
normalize_path_patterns,
@ -535,3 +538,107 @@ class UtilsTests(SimpleTestCase):
def test_normalize_path_patterns_truncates_wildcard_base(self):
expected = [os.path.normcase(p) for p in ["foo/bar", "bar/*/"]]
self.assertEqual(normalize_path_patterns(["foo/bar/*", "bar/*/"]), expected)
def test_get_directory_tree(self):
from django.utils.termcolors import colorize
def format_dir(x):
return colorize(x, fg="blue", opts=["bold"])
with tempfile.TemporaryDirectory() as tmpdir:
subdir = Path(tmpdir) / "foo/bar/"
subdir.mkdir(exist_ok=True, parents=True)
files = [
"file1.txt",
"file2.txt",
"foo/foo1.txt",
"foo/foo2.txt",
"foo/bar/bar1.txt",
"foo/bar/bar2.txt",
]
[Path(tmpdir).joinpath(file).touch() for file in files]
expected = [
format_dir(Path(tmpdir).name),
"|___ file1.txt",
"|___ file2.txt",
f"|___ {format_dir('foo')}",
f" |___ {format_dir('bar')}",
" | |___ bar1.txt",
" | |___ bar2.txt",
" |___ foo1.txt",
" |___ foo2.txt",
]
self.assertListEqual(
list(get_directory_tree(tmpdir, force_color=True)), expected
)
def test_get_directory_tree_no_color(self):
with tempfile.TemporaryDirectory() as tmpdir:
subdir = Path(tmpdir) / "foo/bar/"
subdir.mkdir(exist_ok=True, parents=True)
files = [
"file1.txt",
"file2.txt",
"foo/foo1.txt",
"foo/foo2.txt",
"foo/bar/bar1.txt",
"foo/bar/bar2.txt",
]
[Path(tmpdir).joinpath(file).touch() for file in files]
expected = [
Path(tmpdir).name,
"|___ file1.txt",
"|___ file2.txt",
"|___ foo",
" |___ bar",
" | |___ bar1.txt",
" | |___ bar2.txt",
" |___ foo1.txt",
" |___ foo2.txt",
]
self.assertListEqual(
list(get_directory_tree(tmpdir, no_color=True)), expected
)
def test_get_directory_tree_max_depth(self):
from django.utils.termcolors import colorize
def format_dir(x):
return colorize(x, fg="blue", opts=["bold"])
with tempfile.TemporaryDirectory() as tmpdir:
subdir = Path(tmpdir) / "foo/bar/"
subdir.mkdir(exist_ok=True, parents=True)
files = [
"file1.txt",
"file2.txt",
"foo/foo1.txt",
"foo/foo2.txt",
"foo/bar/bar1.txt",
"foo/bar/bar2.txt",
]
[Path(tmpdir).joinpath(file).touch() for file in files]
expected = [
format_dir(Path(tmpdir).name),
"|___ file1.txt",
"|___ file2.txt",
f"|___ {format_dir('foo')}",
f" |___ {format_dir('bar')}",
" |___ foo1.txt",
" |___ foo2.txt",
]
self.assertListEqual(
list(get_directory_tree(tmpdir, max_depth=2, force_color=True)),
expected,
)
def test_get_directory_tree_fail_not_dir(self):
with tempfile.TemporaryDirectory() as tmpdir:
tmpfile = Path(tmpdir).joinpath("file.txt")
tmpfile.touch()
with self.assertRaises(CommandError):
list(get_directory_tree(tmpfile.name))