1
0
mirror of https://github.com/django/django.git synced 2025-03-31 19:46:42 +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): def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
self.stdout = OutputWrapper(stdout or sys.stdout) self.stdout = OutputWrapper(stdout or sys.stdout)
self.stderr = OutputWrapper(stderr or sys.stderr) self.stderr = OutputWrapper(stderr or sys.stderr)
self.no_color = no_color
if no_color and force_color: if no_color and force_color:
raise CommandError("'no_color' and 'force_color' can't be used together.") raise CommandError("'no_color' and 'force_color' can't be used together.")
if no_color: if no_color:
@ -442,6 +443,7 @@ class BaseCommand:
if options["force_color"]: if options["force_color"]:
self.style = color_style(force_color=True) self.style = color_style(force_color=True)
elif options["no_color"]: elif options["no_color"]:
self.no_color = options["no_color"]
self.style = no_style() self.style = no_style()
self.stderr.style_func = None self.stderr.style_func = None
if options.get("stdout"): 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.base import BaseCommand, CommandError
from django.core.management.utils import ( from django.core.management.utils import (
find_formatters, find_formatters,
get_directory_tree,
handle_extensions, handle_extensions,
run_formatters, run_formatters,
) )
@ -230,6 +231,8 @@ class TemplateCommand(BaseCommand):
shutil.rmtree(path_to_remove) shutil.rmtree(path_to_remove)
run_formatters([top_dir], **formatter_paths) 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): def handle_template(self, template, subdir):
""" """

View File

@ -6,6 +6,7 @@ from pathlib import Path
from subprocess import run from subprocess import run
from django.apps import apps as installed_apps 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.crypto import get_random_string
from django.utils.encoding import DEFAULT_LOCALE_ENCODING 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], [black_path, "--fast", "--", *written_files],
capture_output=True, 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, "usage:")
self.assertOutput(err, "You must provide a project name.") 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): def test_simple_project(self):
"Make sure the startproject management command creates a project" "Make sure the startproject management command creates a project"
args = ["startproject", "testproject"] args = ["startproject", "testproject"]
@ -2879,6 +2904,28 @@ class StartApp(AdminScriptTestCase):
) )
self.assertFalse(os.path.exists(testproject_dir)) 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): def test_importable_name(self):
""" """
startapp validates that app name doesn't clash with existing Python startapp validates that app name doesn't clash with existing Python

View File

@ -1,6 +1,8 @@
import os import os
import tempfile
from argparse import ArgumentDefaultsHelpFormatter from argparse import ArgumentDefaultsHelpFormatter
from io import StringIO from io import StringIO
from pathlib import Path
from unittest import mock from unittest import mock
from admin_scripts.tests import AdminScriptTestCase 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 import BaseCommand, CommandError, find_commands
from django.core.management.utils import ( from django.core.management.utils import (
find_command, find_command,
get_directory_tree,
get_random_secret_key, get_random_secret_key,
is_ignored_path, is_ignored_path,
normalize_path_patterns, normalize_path_patterns,
@ -535,3 +538,107 @@ class UtilsTests(SimpleTestCase):
def test_normalize_path_patterns_truncates_wildcard_base(self): def test_normalize_path_patterns_truncates_wildcard_base(self):
expected = [os.path.normcase(p) for p in ["foo/bar", "bar/*/"]] expected = [os.path.normcase(p) for p in ["foo/bar", "bar/*/"]]
self.assertEqual(normalize_path_patterns(["foo/bar/*", "bar/*/"]), expected) 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))