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:
parent
f515449826
commit
f44a91902b
@ -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"):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user