From ddf1a876b23b189f81fc308a4436d98e20e70d34 Mon Sep 17 00:00:00 2001 From: Luke Jefferies Date: Mon, 18 Aug 2025 11:45:03 +0100 Subject: [PATCH] Fixed #36533 -- Fix startapp to allow empty directories as valid targets. --- django/core/management/templates.py | 15 +++++++- tests/admin_scripts/tests.py | 60 +++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/django/core/management/templates.py b/django/core/management/templates.py index ea2c4a294f..a9669161da 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -105,7 +105,20 @@ class TemplateCommand(BaseCommand): else: top_dir = os.path.abspath(os.path.expanduser(target)) if app_or_project == "app": - self.validate_name(os.path.basename(top_dir), "directory") + dir_name = os.path.basename(top_dir) + + # Non empty directory already exists + if os.path.exists(top_dir): + if os.listdir(top_dir): + raise CommandError( + f"{top_dir} already exists. Overlaying an app into an " + "existing directory won't replace conflicting files." + ) + # Does not exist so validate new name + else: + self.validate_name(dir_name, "directory") + + # Create directory if it doesn't exist if not os.path.exists(top_dir): try: os.makedirs(top_dir) diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 97d2d7cf7e..56eb1f4c73 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -3197,6 +3197,66 @@ class StartApp(AdminScriptTestCase): ) self.assertFalse(os.path.exists(testapp_dir)) + def test_existing_empty_directory_allows_create(self): + """ + Ensure that an existing empty directory + does not trigger a false importable module check. + """ + custom_dir = os.path.join(self.test_dir, "destination") + os.makedirs(custom_dir, exist_ok=True) + + args = ["startapp", "my_app", custom_dir] + _, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertTrue(os.path.exists(os.path.join(custom_dir, "apps.py"))) + + def test_custom_directory_allows_create(self): + """ + Ensure destination can be created as a new directory to regression + test ticket #36533 + """ + custom_dir = os.path.join(self.test_dir, "destination") + args = ["startapp", "my_app", custom_dir] + _, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertTrue(os.path.exists(os.path.join(custom_dir, "apps.py"))) + + def test_importable_python_module_errors(self): + """ + Ensure folder named 'os' can not be created as the name + of an app + """ + args = ["startapp", "example", "os"] + _, err = self.run_django_admin(args) + self.assertIn("CommandError: 'os'", err) + + def test_existing_directory_matching_python_modules_errors(self): + """ + Double checks that an importable module still throws an error if + the user created a directory first with a matching name + """ + os.makedirs("os", exist_ok=True) + args = ["startapp", "example", "os"] + _, err = self.run_django_admin(args) + self.assertIn("CommandError: 'os'", err) + + def test_existing_non_empty_directory_overlay_error(self): + """ + Ensure that an existing non empty directory + triggers an overlaying app warning. + """ + custom_dir = os.path.join(self.test_dir, "non_empty_dir") + os.makedirs(custom_dir, exist_ok=True) + + with open(os.path.join(custom_dir, "dummy.txt"), "w") as f: + f.write("test") + + args = ["startapp", "my_app", custom_dir] + _, err = self.run_django_admin(args) + self.assertIn( + "already exists. Overlaying an app into an existing directory", err + ) + class DiffSettings(AdminScriptTestCase): """Tests for diffsettings management command."""