diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index d6d17aa988..9067e7b605 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -7,7 +7,6 @@ import inspect import math import os import re -import sys import types from importlib import import_module @@ -18,6 +17,7 @@ from django.utils import datetime_safe, six from django.utils._os import upath from django.utils.encoding import force_text from django.utils.functional import Promise +from django.utils.module_loading import module_dir from django.utils.timezone import utc from django.utils.version import get_docs_version @@ -200,44 +200,67 @@ class MigrationWriter(object): value_repr = "datetime.%s" % value_repr return value_repr + @property + def basedir(self): + migrations_package_name = MigrationLoader.migrations_module(self.migration.app_label) + + # See if we can import the migrations module directly + try: + migrations_module = import_module(migrations_package_name) + except ImportError: + pass + else: + try: + return upath(module_dir(migrations_module)) + except ValueError: + pass + + # Alright, see if it's a direct submodule of the app + app_config = apps.get_app_config(self.migration.app_label) + maybe_app_name, _, migrations_package_basename = migrations_package_name.rpartition(".") + if app_config.name == maybe_app_name: + return os.path.join(app_config.path, migrations_package_basename) + + # In case of using MIGRATION_MODULES setting and the custom package + # doesn't exist, create one, starting from an existing package + existing_dirs, missing_dirs = migrations_package_name.split("."), [] + while existing_dirs: + missing_dirs.insert(0, existing_dirs.pop(-1)) + try: + base_module = import_module(".".join(existing_dirs)) + except ImportError: + continue + else: + try: + base_dir = upath(module_dir(base_module)) + except ValueError: + continue + else: + break + else: + raise ValueError( + "Could not locate an appropriate location to create " + "migrations package %s. Make sure the toplevel " + "package exists and can be imported." % + migrations_package_name) + + final_dir = os.path.join(base_dir, *missing_dirs) + if not os.path.isdir(final_dir): + os.makedirs(final_dir) + for missing_dir in missing_dirs: + base_dir = os.path.join(base_dir, missing_dir) + with open(os.path.join(base_dir, "__init__.py"), "w"): + pass + + return final_dir + @property def filename(self): return "%s.py" % self.migration.name @property def path(self): - migrations_package_name = MigrationLoader.migrations_module(self.migration.app_label) - # See if we can import the migrations module directly - try: - migrations_module = import_module(migrations_package_name) - - # Python 3 fails when the migrations directory does not have a - # __init__.py file - if not hasattr(migrations_module, '__file__'): - raise ImportError - - basedir = os.path.dirname(upath(migrations_module.__file__)) - except ImportError: - app_config = apps.get_app_config(self.migration.app_label) - migrations_package_basename = migrations_package_name.split(".")[-1] - - # Alright, see if it's a direct submodule of the app - if '%s.%s' % (app_config.name, migrations_package_basename) == migrations_package_name: - basedir = os.path.join(app_config.path, migrations_package_basename) - else: - # In case of using MIGRATION_MODULES setting and the custom - # package doesn't exist, create one. - package_dirs = migrations_package_name.split(".") - create_path = os.path.join(upath(sys.path[0]), *package_dirs) - if not os.path.isdir(create_path): - os.makedirs(create_path) - for i in range(1, len(package_dirs) + 1): - init_dir = os.path.join(upath(sys.path[0]), *package_dirs[:i]) - init_path = os.path.join(init_dir, "__init__.py") - if not os.path.isfile(init_path): - open(init_path, "w").close() - return os.path.join(create_path, self.filename) - return os.path.join(basedir, self.filename) + return os.path.join(self.basedir, self.filename) @classmethod def serialize_deconstructed(cls, path, args, kwargs): diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index 55d89caa46..5dc3aefe75 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -148,3 +148,21 @@ else: else: # Exhausted the search, so the module cannot be found. return False + + +def module_dir(module): + """ + Find the name of the directory that contains a module, if possible. + + Raise ValueError otherwise, e.g. for namespace packages that are split + over several directories. + """ + # Convert to list because _NamespacePath does not support indexing on 3.3. + paths = list(getattr(module, '__path__', [])) + if len(paths) == 1: + return paths[0] + else: + filename = getattr(module, '__file__', None) + if filename is not None: + return os.path.dirname(filename) + raise ValueError("Cannot determine directory containing %s" % module)