summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Rose <offline@offby1.net>2026-05-18 10:49:34 -0700
committerJacob Walls <jacobtylerwalls@gmail.com>2026-06-01 15:24:49 -0400
commit9383fae0d55a553be3bda620db87fa9ee8a81478 (patch)
tree7c46277248d4ed0fc38eb2193d95c844fc146726
parent22d25eff1510eb157be3ef02984869b1309ec15d (diff)
Fixed #28800 -- Added a listurls management command.
Thanks JaeHyuck Sa, Jacob Walls, and Tim McCurrach for reviews. Co-authored-by: Ülgen Sarıkavak <ulgensrkvk@gmail.com>
-rw-r--r--AUTHORS2
-rw-r--r--django/core/management/commands/listurls.py175
-rw-r--r--django/utils/termcolors.py9
-rw-r--r--docs/ref/django-admin.txt25
-rw-r--r--docs/releases/6.2.txt3
-rw-r--r--tests/admin_scripts/app_with_urls/__init__.py0
-rw-r--r--tests/admin_scripts/app_with_urls/root_urls.py25
-rw-r--r--tests/admin_scripts/app_with_urls/urls_cbv.py17
-rw-r--r--tests/admin_scripts/app_with_urls/urls_namespaced.py17
-rw-r--r--tests/admin_scripts/app_with_urls/urls_nons.py17
-rw-r--r--tests/admin_scripts/app_with_urls/views.py21
-rw-r--r--tests/admin_scripts/tests.py289
12 files changed, 599 insertions, 1 deletions
diff --git a/AUTHORS b/AUTHORS
index a9403842b5..36914d2950 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -229,6 +229,7 @@ answer newbie questions, and generally made Django that much better:
Chris Jerdonek
Chris Jones <chris@brack3t.com>
Chris Lamb <chris@chris-lamb.co.uk>
+ Chris Rose <https://github.com/offbyone>
Chris Streeter <chris@chrisstreeter.com>
Christian Barcenas <christian@cbarcenas.com>
Christian Metts
@@ -1078,6 +1079,7 @@ answer newbie questions, and generally made Django that much better:
Tyson Clugg <tyson@clugg.net>
Tyson Tate <tyson@fallingbullets.com>
Unai Zalakain <unai@gisa-elkartea.org>
+ Ülgen Sarıkavak <https://github.com/ulgens>
Valentina Mukhamedzhanova <umirra@gmail.com>
valtron
Varun Kasyap Pentamaraju <varunkasyap@hotmail.com>
diff --git a/django/core/management/commands/listurls.py b/django/core/management/commands/listurls.py
new file mode 100644
index 0000000000..4cf2d323cf
--- /dev/null
+++ b/django/core/management/commands/listurls.py
@@ -0,0 +1,175 @@
+# Portions of this code are derived from django-extensions (MIT):
+# https://github.com/django-extensions/django-extensions
+
+import json
+from collections import namedtuple
+from importlib import import_module
+
+from django.conf import settings
+from django.core.management import color
+from django.core.management.base import BaseCommand, CommandError, CommandParser
+from django.urls.utils import (
+ extract_views_from_urlpatterns,
+ simplify_regex,
+)
+
+FORMATS = (
+ "tabular",
+ "stacked",
+ "json",
+)
+
+COLORLESS_FORMATS = ("json",)
+
+URLPattern = namedtuple("URLPattern", ["route", "view", "name"])
+
+
+class Command(BaseCommand):
+ help = "List URL patterns in the project with optional filtering by prefixes."
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.style = color.color_style()
+
+ def add_arguments(self, parser: CommandParser):
+ super().add_arguments(parser)
+
+ parser.add_argument(
+ "--unsorted",
+ "-u",
+ action="store_true",
+ dest="unsorted",
+ help="Show URLs without sorting them alphabetically.",
+ )
+ parser.add_argument(
+ "--prefix",
+ "-p",
+ dest="prefixes",
+ help="Only list URLs with these prefixes.",
+ nargs="+",
+ )
+ parser.add_argument(
+ "--format",
+ "-f",
+ choices=FORMATS,
+ default="tabular",
+ dest="format",
+ help="Formatting style of the output",
+ )
+
+ def handle(self, *args, **options):
+ prefixes = options["prefixes"]
+ url_patterns = self.get_url_patterns(prefixes=prefixes)
+ if not url_patterns:
+ raise CommandError("There are no URL patterns that match given prefixes")
+
+ unsorted = options["unsorted"]
+ no_color = options["no_color"]
+ format = options["format"]
+ if not unsorted:
+ url_patterns.sort()
+
+ self.is_color_enabled = (
+ color.supports_color()
+ and (not no_color)
+ and (format not in COLORLESS_FORMATS)
+ )
+ if self.is_color_enabled:
+ url_patterns = self.apply_color(url_patterns=url_patterns)
+
+ url_patterns = self.apply_format(url_patterns=url_patterns, format=format)
+ return url_patterns
+
+ @classmethod
+ def get_url_patterns(cls, prefixes=None):
+ """
+ Returns a list of URL patterns in the project with given prefixes.
+
+ Each object in the returned list is a tuple[str, str, str]:
+ (route, view, name).
+ """
+ url_patterns = []
+ urlconf = import_module(settings.ROOT_URLCONF)
+
+ for view_func, regex, namespace, name in extract_views_from_urlpatterns(
+ urlconf.urlpatterns
+ ):
+ route = simplify_regex(regex)
+
+ if hasattr(view_func, "view_class"):
+ view_func = view_func.view_class
+
+ view = "{}.{}".format(
+ view_func.__module__,
+ getattr(view_func, "__name__", view_func.__class__.__name__),
+ )
+ namespace_list = namespace or []
+ name = ":".join(namespace_list + [name]) if name else ""
+
+ pattern = URLPattern(route, view, name)
+ if not prefixes or any(
+ pattern.route.startswith(prefix) for prefix in prefixes
+ ):
+ url_patterns.append(pattern)
+
+ return url_patterns
+
+ def apply_color(self, url_patterns):
+ colored_url_patterns = []
+
+ for url_pattern in url_patterns:
+ route = self.style.COMMAND_DATA(url_pattern.route)
+
+ module_path, module_name = url_pattern.view.rsplit(".", 1)
+ module_name = self.style.COMMAND_HIGHLIGHT(module_name)
+ view = f"{module_path}.{module_name}"
+
+ if name := url_pattern.name:
+ namespace, name = name.rsplit(":", 1) if ":" in name else ("", name)
+ name = self.style.COMMAND_HIGHLIGHT(name)
+ name = f"{namespace}:{name}" if namespace else name
+
+ colored_url_patterns.append((route, view, name))
+
+ return colored_url_patterns
+
+ def apply_format(self, url_patterns, format):
+ format_method_name = f"format_{format.replace('-', '_')}"
+ format_method = getattr(self, format_method_name)
+ return format_method(url_patterns)
+
+ def format_tabular(self, url_patterns):
+ widths = []
+ margin = 2
+ for columns in zip(*url_patterns, strict=False):
+ widths.append(len(max(columns, key=len)) + margin)
+
+ lines = []
+ for row in url_patterns:
+ line = "".join(
+ cdata.ljust(width) for width, cdata in zip(widths, row, strict=False)
+ )
+ lines.append(line)
+
+ return "\n".join(lines)
+
+ def format_stacked(self, url_patterns):
+ separator = "-" * 20
+ apply_style = (
+ self.style.COMMAND_HEADER if self.is_color_enabled else lambda text: text
+ )
+
+ lines = []
+ for route, view, name in url_patterns:
+ lines.append(apply_style("Route: ") + route)
+ lines.append(apply_style("View: ") + view)
+ if name:
+ lines.append(apply_style("Name: ") + name)
+ lines.append(separator)
+
+ return "\n".join(lines)
+
+ def format_json(self, url_patterns):
+ url_pattern_dicts = [url_pattern._asdict() for url_pattern in url_patterns]
+ return json.dumps(url_pattern_dicts, indent=2)
diff --git a/django/utils/termcolors.py b/django/utils/termcolors.py
index aeef02f1b0..26df7b76d0 100644
--- a/django/utils/termcolors.py
+++ b/django/utils/termcolors.py
@@ -97,6 +97,9 @@ PALETTES = {
"HTTP_SERVER_ERROR": {},
"MIGRATE_HEADING": {},
"MIGRATE_LABEL": {},
+ "COMMAND_HEADER": {},
+ "COMMAND_DATA": {},
+ "COMMAND_HIGHLIGHT": {},
},
DARK_PALETTE: {
"ERROR": {"fg": "red", "opts": ("bold",)},
@@ -116,6 +119,9 @@ PALETTES = {
"HTTP_SERVER_ERROR": {"fg": "magenta", "opts": ("bold",)},
"MIGRATE_HEADING": {"fg": "cyan", "opts": ("bold",)},
"MIGRATE_LABEL": {"opts": ("bold",)},
+ "COMMAND_HEADER": {"fg": "cyan", "opts": ("bold",)},
+ "COMMAND_DATA": {"opts": ("bold",)},
+ "COMMAND_HIGHLIGHT": {"fg": "yellow", "opts": ("bold",)},
},
LIGHT_PALETTE: {
"ERROR": {"fg": "red", "opts": ("bold",)},
@@ -135,6 +141,9 @@ PALETTES = {
"HTTP_SERVER_ERROR": {"fg": "magenta", "opts": ("bold",)},
"MIGRATE_HEADING": {"fg": "cyan", "opts": ("bold",)},
"MIGRATE_LABEL": {"opts": ("bold",)},
+ "COMMAND_HEADER": {"fg": "cyan", "opts": ("bold",)},
+ "COMMAND_DATA": {"opts": ("bold",)},
+ "COMMAND_HIGHLIGHT": {"fg": "yellow", "opts": ("bold",)},
},
}
DEFAULT_PALETTE = DARK_PALETTE
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index adbd2465a7..fc8de971ec 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -499,6 +499,31 @@ Only support for PostgreSQL is implemented.
If this option is provided, models are also created for database views.
+``listurls``
+------------
+
+.. django-admin:: listurls
+
+.. versionadded:: 6.2
+
+List URL patterns from the project's root :doc:`URLconf </topics/http/urls>`
+with optional filtering by prefixes, inspired by ``show_urls`` from
+``django-extensions``.
+
+.. django-admin-option:: --unsorted, -u
+
+Lists URLs in the original ordering from ``urlpatterns``. The default ordering
+is alphabetical by route.
+
+.. django-admin-option:: --prefix, -p [prefix ...]
+
+Filters URLs by given prefixes.
+
+.. django-admin-option:: --format, -f {tabular,stacked,json}
+
+Specifies the output format. Available values are ``tabular``, ``stacked``, and
+``json``. Default is ``tabular``.
+
``loaddata``
------------
diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt
index 3bafa4f583..f88beb6fa0 100644
--- a/docs/releases/6.2.txt
+++ b/docs/releases/6.2.txt
@@ -173,7 +173,8 @@ Logging
Management Commands
~~~~~~~~~~~~~~~~~~~
-* ...
+* The new :djadmin:`listurls` command lists the URLs from the project's root
+ URLconf, including the view class or function (and name, if present).
Migrations
~~~~~~~~~~
diff --git a/tests/admin_scripts/app_with_urls/__init__.py b/tests/admin_scripts/app_with_urls/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/admin_scripts/app_with_urls/__init__.py
diff --git a/tests/admin_scripts/app_with_urls/root_urls.py b/tests/admin_scripts/app_with_urls/root_urls.py
new file mode 100644
index 0000000000..df14128138
--- /dev/null
+++ b/tests/admin_scripts/app_with_urls/root_urls.py
@@ -0,0 +1,25 @@
+from django.urls import include, path, re_path
+
+
+def dummy_view(request): ...
+
+
+urlpatterns = [
+ path(
+ route="nons/",
+ view=include("admin_scripts.app_with_urls.urls_nons"),
+ ),
+ path(
+ route="namespaced/",
+ view=include("admin_scripts.app_with_urls.urls_namespaced", namespace="ns"),
+ ),
+ path(
+ route="cbv/",
+ view=include("admin_scripts.app_with_urls.urls_cbv"),
+ ),
+ re_path(
+ r"^\.well-known/openid-configuration/?$",
+ dummy_view,
+ name="oidc-connect-discovery-info",
+ ),
+]
diff --git a/tests/admin_scripts/app_with_urls/urls_cbv.py b/tests/admin_scripts/app_with_urls/urls_cbv.py
new file mode 100644
index 0000000000..ec4740f5b9
--- /dev/null
+++ b/tests/admin_scripts/app_with_urls/urls_cbv.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from . import views
+
+app_name = "app_with_urls_cbv"
+
+urlpatterns = [
+ path(
+ route="unnamed",
+ view=views.CBV.as_view(),
+ ),
+ path(
+ route="named",
+ view=views.CBV.as_view(),
+ name="named",
+ ),
+]
diff --git a/tests/admin_scripts/app_with_urls/urls_namespaced.py b/tests/admin_scripts/app_with_urls/urls_namespaced.py
new file mode 100644
index 0000000000..a993271c79
--- /dev/null
+++ b/tests/admin_scripts/app_with_urls/urls_namespaced.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from . import views
+
+app_name = "app_with_urls"
+
+urlpatterns = [
+ path(
+ route="unnamed",
+ view=views.view_func_namespaced_unnamed,
+ ),
+ path(
+ route="named",
+ view=views.view_func_namespaced_named,
+ name="named",
+ ),
+]
diff --git a/tests/admin_scripts/app_with_urls/urls_nons.py b/tests/admin_scripts/app_with_urls/urls_nons.py
new file mode 100644
index 0000000000..e957d09fa2
--- /dev/null
+++ b/tests/admin_scripts/app_with_urls/urls_nons.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from . import views
+
+app_name = "app_with_urls"
+
+urlpatterns = [
+ path(
+ route="unnamed",
+ view=views.view_func_nons_unnamed,
+ ),
+ path(
+ route="named",
+ view=views.view_func_nons_named,
+ name="named",
+ ),
+]
diff --git a/tests/admin_scripts/app_with_urls/views.py b/tests/admin_scripts/app_with_urls/views.py
new file mode 100644
index 0000000000..64035868d4
--- /dev/null
+++ b/tests/admin_scripts/app_with_urls/views.py
@@ -0,0 +1,21 @@
+from django.views.generic import ListView
+
+
+def view_func_namespaced_unnamed(request):
+ pass
+
+
+def view_func_namespaced_named(request):
+ pass
+
+
+def view_func_nons_unnamed(request):
+ pass
+
+
+def view_func_nons_named(request):
+ pass
+
+
+class CBV(ListView):
+ pass
diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py
index 914f54720c..819ba931d6 100644
--- a/tests/admin_scripts/tests.py
+++ b/tests/admin_scripts/tests.py
@@ -4,6 +4,7 @@ advertised - especially with regards to the handling of the
DJANGO_SETTINGS_MODULE and default settings.py files.
"""
+import json
import os
import re
import shutil
@@ -30,6 +31,7 @@ from django.core.management import (
execute_from_command_line,
)
from django.core.management.base import LabelCommand, SystemCheckError
+from django.core.management.commands.listurls import Command as ListurlsCommand
from django.core.management.commands.loaddata import Command as LoaddataCommand
from django.core.management.commands.runserver import Command as RunserverCommand
from django.core.management.commands.testserver import Command as TestserverCommand
@@ -3313,6 +3315,293 @@ class Dumpdata(AdminScriptTestCase):
self.assertNoOutput(out)
+@override_settings(ROOT_URLCONF="admin_scripts.app_with_urls.root_urls")
+class Listurls(AdminScriptTestCase):
+ def test_default(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ args = ["listurls"]
+ out, err = self.run_manage(args)
+
+ self.assertNoOutput(err)
+
+ # Check route, view and (if defined) name for each URL.
+ self.assertOutput(out, "/namespaced/named")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_namespaced_named",
+ )
+ self.assertOutput(out, "ns:named")
+
+ self.assertOutput(out, "/namespaced/unnamed")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_namespaced_unnamed",
+ )
+
+ self.assertOutput(out, "/nons/named")
+ self.assertOutput(out, "admin_scripts.app_with_urls.views.view_func_nons_named")
+ self.assertOutput(out, "app_with_urls:named")
+
+ self.assertOutput(out, "/nons/unnamed")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_nons_unnamed",
+ )
+
+ def test_urls_with_metachars(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ args = ["listurls", "--prefix", "/.well-known"]
+ out, err = self.run_manage(args)
+
+ self.assertOutput(out, "/.well-known/openid-configuration/")
+ self.assertNoOutput(err)
+
+ def test_cbv_formatting(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ args = ["listurls", "--prefix", "/cbv"]
+ out, err = self.run_manage(args)
+
+ self.assertOutput(out, "/cbv/named")
+ self.assertOutput(out, "admin_scripts.app_with_urls.views.CBV")
+ self.assertOutput(out, "app_with_urls_cbv:named")
+ self.assertNoOutput(err)
+
+ def test_tabular(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ args = ["listurls", "-f", "tabular"]
+ out, err = self.run_manage(args)
+ self.assertNoOutput(err)
+
+ # Check route, view and (if defined) name for each URL.
+ self.assertOutput(out, "/namespaced/named")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_namespaced_named",
+ )
+ self.assertOutput(out, "ns:named")
+
+ self.assertOutput(out, "/namespaced/unnamed")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_namespaced_unnamed",
+ )
+
+ self.assertOutput(out, "/nons/named")
+ self.assertOutput(out, "admin_scripts.app_with_urls.views.view_func_nons_named")
+ self.assertOutput(out, "app_with_urls:named")
+
+ self.assertOutput(out, "/nons/unnamed")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_nons_unnamed",
+ )
+
+ def test_stacked(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ args = ["listurls", "-f", "stacked"]
+ out, err = self.run_manage(args)
+ self.assertNoOutput(err)
+
+ self.assertOutput(out, "Route:")
+ self.assertOutput(out, "View:")
+ self.assertOutput(out, "Name:")
+ self.assertOutput(out, "-" * 20)
+
+ self.assertOutput(out, "/namespaced/named")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_namespaced_named",
+ )
+ self.assertOutput(out, "ns:named")
+
+ self.assertOutput(out, "/namespaced/unnamed")
+ self.assertOutput(
+ out, "admin_scripts.app_with_urls.views.view_func_namespaced_unnamed"
+ )
+
+ self.assertOutput(out, "/nons/named")
+ self.assertOutput(out, "app_with_urls:named")
+ self.assertOutput(out, "admin_scripts.app_with_urls.views.view_func_nons_named")
+
+ self.assertOutput(out, "/nons/unnamed")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_nons_unnamed",
+ )
+
+ def test_json(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ args = ["listurls", "-f", "json"]
+ out, err = self.run_manage(args)
+ self.assertNoOutput(err)
+
+ json.loads(out)
+
+ self.assertOutput(out, '"route": "/namespaced/named"')
+ self.assertOutput(
+ out,
+ '"view": "admin_scripts.app_with_urls.views.view_func_namespaced_named"',
+ )
+ self.assertOutput(out, '"name": "ns:named"')
+
+ self.assertOutput(out, '"route": "/namespaced/unnamed"')
+ self.assertOutput(
+ out,
+ '"view": "admin_scripts.app_with_urls.views.view_func_namespaced_unnamed"',
+ )
+
+ self.assertOutput(out, '"route": "/nons/named"')
+ self.assertOutput(out, "admin_scripts.app_with_urls.views.view_func_nons_named")
+ self.assertOutput(out, "app_with_urls:named")
+
+ self.assertOutput(out, "/nons/unnamed")
+ self.assertOutput(
+ out,
+ "admin_scripts.app_with_urls.views.view_func_nons_unnamed",
+ )
+
+ def test_unsorted(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ # JSON format is the easiest to parse and test.
+ args = ["listurls", "-f", "json", "--unsorted"]
+ out, err = self.run_manage(args)
+ url_patterns = json.loads(out)
+
+ self.assertNotEqual(
+ url_patterns,
+ sorted(url_patterns, key=lambda u: u["route"]),
+ )
+
+ def test_tabular_with_color_enabled(self):
+ out = StringIO()
+ err = StringIO()
+
+ with mock.patch(
+ "django.core.management.color.supports_color", lambda *args: True
+ ):
+ command = ListurlsCommand(stdout=out, stderr=err)
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+ call_command(command, format="tabular")
+
+ self.assertIn(command.style.COMMAND_DATA("/namespaced/named"), out.getvalue())
+ self.assertIn(
+ command.style.COMMAND_HIGHLIGHT("view_func_namespaced_named"),
+ out.getvalue(),
+ )
+ self.assertIn(command.style.COMMAND_HIGHLIGHT("named"), out.getvalue())
+
+ def test_tabular_with_color_suppressed(self):
+ out = StringIO()
+ err = StringIO()
+
+ with mock.patch(
+ "django.core.management.color.supports_color", lambda *args: True
+ ):
+ command = ListurlsCommand(stdout=out, stderr=err)
+ call_command(command, format="tabular", no_color=True)
+
+ self.assertIn("/namespaced/named", out.getvalue())
+
+ # There should be no escape codes in the output.
+ self.assertNotIn("\x1b", out.getvalue())
+
+ def test_stacked_with_color_enabled(self):
+ out = StringIO()
+ err = StringIO()
+
+ with mock.patch(
+ "django.core.management.color.supports_color", lambda *args: True
+ ):
+ command = ListurlsCommand(stdout=out, stderr=err)
+ call_command(command, format="stacked")
+
+ self.assertIn(command.style.COMMAND_DATA("/namespaced/named"), out.getvalue())
+ self.assertIn(
+ command.style.COMMAND_HIGHLIGHT("view_func_namespaced_named"),
+ out.getvalue(),
+ )
+ self.assertIn(command.style.COMMAND_HIGHLIGHT("named"), out.getvalue())
+ for header in ["Route", "View", "Name"]:
+ with self.subTest(header=header):
+ self.assertIn(
+ command.style.COMMAND_HEADER(f"{header}: "), out.getvalue()
+ )
+
+ def test_stacked_with_color_suppressed(self):
+ out = StringIO()
+ err = StringIO()
+
+ with mock.patch(
+ "django.core.management.color.supports_color", lambda *args: True
+ ):
+ command = ListurlsCommand(stdout=out, stderr=err)
+ call_command(command, format="stacked", no_color=True)
+
+ self.assertIn("/namespaced/named", out.getvalue())
+
+ # There should be no escape codes in the output.
+ self.assertNotIn("\x1b", out.getvalue())
+
+ @override_settings(ROOT_URLCONF="urls")
+ def test_no_urls(self):
+ self.write_settings("settings.py")
+
+ args = ["listurls"]
+ out, err = self.run_manage(args)
+
+ self.assertOutput(err, "There are no URL patterns that match given prefixes")
+ self.assertNoOutput(out)
+
+ def test_prefixes(self):
+ self.write_settings(
+ "settings.py",
+ apps=["admin_scripts.app_with_urls"],
+ )
+
+ args = ["listurls", "-p", "/namespaced"]
+ out, err = self.run_manage(args)
+ self.assertNoOutput(err)
+
+ self.assertOutput(out, "/namespaced/named")
+ self.assertOutput(out, "ns:named")
+ self.assertOutput(out, "/namespaced/unnamed")
+
+ self.assertNotInOutput(out, "/nons/named")
+ self.assertNotInOutput(out, "app_with_urls:named")
+ self.assertNotInOutput(out, "/nons/unnamed")
+
+
class MainModule(AdminScriptTestCase):
"""python -m django works like django-admin."""