From b9c619abc101688fbbfa981525175f831d359483 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 22 Feb 2015 21:18:12 +0100 Subject: [PATCH] Prevented makemigrations from writing in sys.path[0]. There's no reason to assume that sys.path[0] is an appropriate location for generating code. Specifically that doesn't work with extend_sys_path which puts the additional directories at the end of sys.path. In order to create a new migrations module, instead of using an arbitrary entry from sys.path, import as much as possible from the path to the module, then create missing submodules from there. Without this change, the tests introduced in the following commit fail, which seems sufficient to prevent regressions for such a refactoring. --- django/db/migrations/writer.py | 89 +++++++++++++++++++++------------- django/utils/module_loading.py | 18 +++++++ 2 files changed, 74 insertions(+), 33 deletions(-) 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)