diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index b88026a335..a0c86024b9 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import collections from importlib import import_module import os +import pkgutil import sys import django @@ -24,11 +25,10 @@ def find_commands(management_dir): Returns an empty list if no commands are defined. """ command_dir = os.path.join(management_dir, 'commands') - try: - return [f[:-3] for f in os.listdir(command_dir) - if not f.startswith('_') and f.endswith('.py')] - except OSError: - return [] + # Workaround for a Python 3.2 bug with pkgutil.iter_modules + sys.path_importer_cache.pop(command_dir, None) + return [name for _, name, is_pkg in pkgutil.iter_modules([command_dir]) + if not is_pkg and not name.startswith('_')] def load_command_class(app_name, name): diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index b69e69db49..d6e139606d 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -371,6 +371,8 @@ Management Commands * Database connections are now always closed after a management command called from the command line has finished doing its job. +* Commands from alternate package formats like eggs are now also discovered. + * :djadmin:`dumpdata` now has the option :djadminopt:`--output` which allows specifying the file to which the serialized data is written. diff --git a/tests/user_commands/eggs/basic.egg b/tests/user_commands/eggs/basic.egg new file mode 100644 index 0000000000..cb25c6d8cf Binary files /dev/null and b/tests/user_commands/eggs/basic.egg differ diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 6a5677f2c9..dff7dcaed5 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -1,13 +1,15 @@ import os +from django.apps import apps from django.db import connection from django.core import management -from django.core.management import BaseCommand, CommandError +from django.core.management import BaseCommand, CommandError, find_commands from django.core.management.utils import find_command, popen_wrapper from django.test import SimpleTestCase, ignore_warnings -from django.test.utils import captured_stderr, captured_stdout +from django.test.utils import captured_stderr, captured_stdout, extend_sys_path from django.utils import translation from django.utils.deprecation import RemovedInDjango20Warning +from django.utils._os import upath from django.utils.six import StringIO @@ -72,6 +74,17 @@ class CommandTests(SimpleTestCase): if current_path is not None: os.environ['PATH'] = current_path + def test_discover_commands_in_eggs(self): + """ + Test that management commands can also be loaded from Python eggs. + """ + egg_dir = '%s/eggs' % os.path.dirname(upath(__file__)) + egg_name = '%s/basic.egg' % egg_dir + with extend_sys_path(egg_name): + with self.settings(INSTALLED_APPS=['commandegg']): + cmds = find_commands(os.path.join(apps.get_app_config('commandegg').path, 'management')) + self.assertEqual(cmds, ['eggcommand']) + def test_call_command_option_parsing(self): """ When passing the long option name to call_command, the available option