From 074844e9479a9ee1420685b36189aa2881289993 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Fri, 1 May 2020 15:05:20 +0100 Subject: [PATCH] Fixed #31529 -- Added support for serialization of pathlib.Path/PurePath and os.PathLike in migrations. --- django/db/migrations/serializer.py | 17 +++++++++++++ docs/releases/3.2.txt | 3 +++ docs/topics/migrations.txt | 10 ++++++++ tests/migrations/test_writer.py | 41 ++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/django/db/migrations/serializer.py b/django/db/migrations/serializer.py index ead81c398a..e19c881cda 100644 --- a/django/db/migrations/serializer.py +++ b/django/db/migrations/serializer.py @@ -5,6 +5,8 @@ import decimal import enum import functools import math +import os +import pathlib import re import types import uuid @@ -217,6 +219,19 @@ class OperationSerializer(BaseSerializer): return string.rstrip(','), imports +class PathLikeSerializer(BaseSerializer): + def serialize(self): + return repr(os.fspath(self.value)), {} + + +class PathSerializer(BaseSerializer): + def serialize(self): + # Convert concrete paths to pure paths to avoid issues with migrations + # generated on one platform being used on a different platform. + prefix = 'Pure' if isinstance(self.value, pathlib.Path) else '' + return 'pathlib.%s%r' % (prefix, self.value), {'import pathlib'} + + class RegexSerializer(BaseSerializer): def serialize(self): regex_pattern, pattern_imports = serializer_factory(self.value.pattern).serialize() @@ -298,6 +313,8 @@ class Serializer: collections.abc.Iterable: IterableSerializer, (COMPILED_REGEX_TYPE, RegexObject): RegexSerializer, uuid.UUID: UUIDSerializer, + pathlib.PurePath: PathSerializer, + os.PathLike: PathLikeSerializer, } @classmethod diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 3cc39ff285..8544df77c3 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -200,6 +200,9 @@ Migrations filename fragment that will be used to name a migration containing only that operation. +* Migrations now support serialization of pure and concrete path objects from + :mod:`pathlib`, and :class:`os.PathLike` instances. + Models ~~~~~~ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index 2d609f01da..b0d75a4f09 100644 --- a/docs/topics/migrations.txt +++ b/docs/topics/migrations.txt @@ -720,6 +720,11 @@ Django can serialize the following: - ``uuid.UUID`` instances - :func:`functools.partial` and :class:`functools.partialmethod` instances which have serializable ``func``, ``args``, and ``keywords`` values. +- Pure and concrete path objects from :mod:`pathlib`. Concrete paths are + converted to their pure path equivalent, e.g. :class:`pathlib.PosixPath` to + :class:`pathlib.PurePosixPath`. +- :class:`os.PathLike` instances, e.g. :class:`os.DirEntry`, which are + converted to ``str`` or ``bytes`` using :func:`os.fspath`. - ``LazyObject`` instances which wrap a serializable value. - Enumeration types (e.g. ``TextChoices`` or ``IntegerChoices``) instances. - Any Django field @@ -728,6 +733,11 @@ Django can serialize the following: - Any class reference (must be in module's top-level scope) - Anything with a custom ``deconstruct()`` method (:ref:`see below `) +.. versionchanged:: 3.2 + + Serialization support for pure and concrete path objects from + :mod:`pathlib`, and :class:`os.PathLike` instances was added. + Django cannot serialize: - Nested classes diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 6a65e87d5a..5635dc62f0 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -4,7 +4,9 @@ import enum import functools import math import os +import pathlib import re +import sys import uuid from unittest import mock @@ -429,6 +431,45 @@ class WriterTests(SimpleTestCase): "default=uuid.UUID('5c859437-d061-4847-b3f7-e6b78852f8c8'))" ) + def test_serialize_pathlib(self): + # Pure path objects work in all platforms. + self.assertSerializedEqual(pathlib.PurePosixPath()) + self.assertSerializedEqual(pathlib.PureWindowsPath()) + path = pathlib.PurePosixPath('/path/file.txt') + expected = ("pathlib.PurePosixPath('/path/file.txt')", {'import pathlib'}) + self.assertSerializedResultEqual(path, expected) + path = pathlib.PureWindowsPath('A:\\File.txt') + expected = ("pathlib.PureWindowsPath('A:/File.txt')", {'import pathlib'}) + self.assertSerializedResultEqual(path, expected) + # Concrete path objects work on supported platforms. + if sys.platform == 'win32': + self.assertSerializedEqual(pathlib.WindowsPath.cwd()) + path = pathlib.WindowsPath('A:\\File.txt') + expected = ("pathlib.PureWindowsPath('A:/File.txt')", {'import pathlib'}) + self.assertSerializedResultEqual(path, expected) + else: + self.assertSerializedEqual(pathlib.PosixPath.cwd()) + path = pathlib.PosixPath('/path/file.txt') + expected = ("pathlib.PurePosixPath('/path/file.txt')", {'import pathlib'}) + self.assertSerializedResultEqual(path, expected) + + field = models.FilePathField(path=pathlib.PurePosixPath('/home/user')) + string, imports = MigrationWriter.serialize(field) + self.assertEqual( + string, + "models.FilePathField(path=pathlib.PurePosixPath('/home/user'))", + ) + self.assertIn('import pathlib', imports) + + def test_serialize_path_like(self): + path_like = list(os.scandir(os.path.dirname(__file__)))[0] + expected = (repr(path_like.path), {}) + self.assertSerializedResultEqual(path_like, expected) + + field = models.FilePathField(path=path_like) + string = MigrationWriter.serialize(field)[0] + self.assertEqual(string, 'models.FilePathField(path=%r)' % path_like.path) + def test_serialize_functions(self): with self.assertRaisesMessage(ValueError, 'Cannot serialize function: lambda'): self.assertSerializedEqual(lambda x: 42)