Skip to content

Commit 609ab5e

Browse files
committed
Auto coercion, now you can assign the file directly to the model's attribute, closes #98
1 parent d588fe6 commit 609ab5e

File tree

2 files changed

+84
-41
lines changed

2 files changed

+84
-41
lines changed

sqlalchemy_media/attachments.py

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,6 @@ def _listen_on_attribute(cls, attribute, coerce, parent_cls):
7474
StoreManager.observe_attribute(attribute)
7575
super()._listen_on_attribute(attribute, coerce, parent_cls)
7676

77-
@classmethod
78-
def _assert_type(cls, value) -> None:
79-
"""
80-
Checking attachment type, raising :exc:`TypeError` if the value is not derived from :class:`.Attachment`.
81-
82-
"""
83-
if not isinstance(value, cls):
84-
raise TypeError('Value type must be subclass of %s, but it\'s: %s' % (cls, type(value)))
85-
8677
@classmethod
8778
def coerce(cls, key, value) -> 'Attachment':
8879
"""
@@ -91,15 +82,19 @@ def coerce(cls, key, value) -> 'Attachment':
9182
.. seealso:: :meth:`sqlalchemy.ext.mutable.MutableDict.coerce`
9283
9384
"""
94-
if value is not None and not isinstance(value, dict):
95-
try:
96-
cls._assert_type(value)
97-
except TypeError:
98-
if cls.__auto_coercion__:
99-
return cls.create_from(value)
100-
raise
85+
if value is None or isinstance(value, (cls, dict)):
86+
return super().coerce(key, value)
10187

102-
return super().coerce(key, value)
88+
if cls.__auto_coercion__:
89+
if not isinstance(value, (tuple, list)):
90+
value = (value, )
91+
92+
return cls.create_from(*value)
93+
94+
raise TypeError(
95+
'Value type must be subclass of %s or a tuple(file, mime, [filename]),'
96+
'but it\'s: %s' % (cls, type(value))
97+
)
10398

10499
@classmethod
105100
def create_from(cls, *args, **kwargs):
@@ -168,7 +163,7 @@ def path(self) -> str:
168163
def filename(self) -> str:
169164
"""
170165
The filename used to store the attachment in the storage with this format::
171-
166+
172167
'{self.__prefix__}-{self.key}{self.suffix}{if self.extension else ''}'
173168
174169
:type: str
@@ -250,14 +245,14 @@ def reproducible(self) -> bool:
250245
def copy(self) -> 'Attachment':
251246
"""
252247
Copy this object using deepcopy.
253-
248+
254249
"""
255250
return self.__class__(copy.deepcopy(self))
256251

257252
def get_store(self) -> Store:
258253
"""
259254
Returns the :class:`sqlalchemy_media.stores.Store` instance, which this file is stored on.
260-
255+
261256
"""
262257
store_manager = StoreManager.get_current_store_manager()
263258
return store_manager.get(self.store_id)
@@ -269,7 +264,7 @@ def delete(self) -> None:
269264
.. warning:: This operation can not be roll-backed.So if you want to delete a file, just set it to
270265
:const:`None` or set it by new :class:`.Attachment` instance, while passed ``delete_orphan=True``
271266
in :class:`.StoreManager`.
272-
267+
273268
"""
274269
self.get_store().delete(self.path)
275270

@@ -319,7 +314,7 @@ def attach(self, attachable: Attachable, content_type: str = None, original_file
319314
320315
:param attachable: file-like object, filename or URL to attach.
321316
:param content_type: If given, the content-detection is suppressed.
322-
:param original_filename: Original name of the file, if available, to append to the end of the the filename,
317+
:param original_filename: Original name of the file, if available, to append to the end of the the filename,
323318
useful for SEO, and readability.
324319
:param extension: The file's extension, is available.else, tries to guess it by content_type
325320
:param store_id: The store id to store this file on. Stores must be registered with appropriate id via
@@ -427,13 +422,13 @@ def get_objects_to_delete(self):
427422

428423
def get_orphaned_objects(self):
429424
"""
430-
this method will be always called by the store when adding the ``self`` to the orphaned list. so subclasses
431-
of the :class:`.Attachment` has a chance to add other related objects into the orphaned list and schedule it
432-
for delete. for example the :class:`.Image` class can schedule it's thumbnails for deletion also.
425+
this method will be always called by the store when adding the ``self`` to the orphaned list. so subclasses
426+
of the :class:`.Attachment` has a chance to add other related objects into the orphaned list and schedule it
427+
for delete. for example the :class:`.Image` class can schedule it's thumbnails for deletion also.
433428
:return: An iterable of :class:`.Attachment` to mark as orphan.
434-
429+
435430
.. versionadded:: 0.11.0
436-
431+
437432
"""
438433
return iter([])
439434

@@ -477,18 +472,18 @@ class Person(BaseModel):
477472
def observe_item(self, item):
478473
"""
479474
A simple monkeypatch to instruct the children to notify the parent if contents are changed:
480-
475+
481476
From `sqlalchemy mutable documentation:
482477
<http://docs.sqlalchemy.org/en/latest/orm/extensions/mutable.html#sqlalchemy.ext.mutable.MutableList>`_
483-
484-
Note that MutableList does not apply mutable tracking to the values themselves inside the list. Therefore
485-
it is not a sufficient solution for the use case of tracking deep changes to a recursive mutable structure,
486-
such as a JSON structure. To support this use case, build a subclass of MutableList that provides
487-
appropriate coercion to the values placed in the dictionary so that they too are “mutable”, and emit events
478+
479+
Note that MutableList does not apply mutable tracking to the values themselves inside the list. Therefore
480+
it is not a sufficient solution for the use case of tracking deep changes to a recursive mutable structure,
481+
such as a JSON structure. To support this use case, build a subclass of MutableList that provides
482+
appropriate coercion to the values placed in the dictionary so that they too are “mutable”, and emit events
488483
up to their parent structure.
489-
484+
490485
:param item: The item to observe
491-
:return:
486+
:return:
492487
"""
493488
item = self.__item_type__.coerce(None, item)
494489
item._parents = self._parents
@@ -574,7 +569,7 @@ class Person(BaseModel):
574569
me = Person()
575570
me.files = MyDict()
576571
me.files['original'] = MyAttachment.create_from(any_file)
577-
572+
578573
"""
579574

580575
@classmethod
@@ -627,7 +622,7 @@ def __setitem__(self, key, value):
627622
class File(Attachment):
628623
"""
629624
Representing an attached file. Normally if you want to store any file, this class is the best choice.
630-
625+
631626
"""
632627

633628
__directory__ = 'files'
@@ -675,7 +670,7 @@ def attach(self, *args, dimension: Dimension=None, **kwargs):
675670
:param kwargs: The same as the: :meth:`.Attachment.attach`.
676671
677672
:return: The same as the: :meth:`.Attachment.attach`.
678-
673+
679674
"""
680675
if dimension:
681676
kwargs['width'], kwargs['height'] = dimension
@@ -854,9 +849,9 @@ def get_objects_to_delete(self):
854849

855850
def get_orphaned_objects(self):
856851
"""
857-
Mark thumbnails for deletion when the :class:`.Image` is being deleted.
852+
Mark thumbnails for deletion when the :class:`.Image` is being deleted.
858853
:return: An iterable of :class:`.Thumbnail` to mark as orphan.
859-
854+
860855
.. versionadded:: 0.11.0
861856
862857
"""
@@ -870,7 +865,7 @@ def get_orphaned_objects(self):
870865
class ImageList(AttachmentList):
871866
"""
872867
Used to create a collection of :class:`.Image`es
873-
868+
874869
.. versionadded:: 0.11.0
875870
"""
876871

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import unittest
2+
import io
3+
from os.path import join, exists
4+
5+
from sqlalchemy import Column, Integer
6+
7+
from sqlalchemy_media.attachments import File
8+
from sqlalchemy_media.stores import StoreManager
9+
from sqlalchemy_media.tests.helpers import Json, TempStoreTestCase
10+
11+
12+
class AutoCoerceFile(File):
13+
__auto_coercion__ = True
14+
15+
16+
class GenericAssignmentTestCase(TempStoreTestCase):
17+
18+
def test_file_assignment(self):
19+
20+
class Person(self.Base):
21+
__tablename__ = 'person'
22+
id = Column(Integer, primary_key=True)
23+
cv = Column(AutoCoerceFile.as_mutable(Json))
24+
25+
session = self.create_all_and_get_session()
26+
person1 = Person()
27+
resume = io.BytesIO(b'This is my resume')
28+
with StoreManager(session):
29+
person1.cv = resume
30+
self.assertIsNone(person1.cv.content_type)
31+
self.assertIsNone(person1.cv.extension)
32+
self.assertTrue(exists(join(self.temp_path, person1.cv.path)))
33+
34+
person1.cv = resume, 'text/plain'
35+
self.assertEqual(person1.cv.content_type, 'text/plain')
36+
self.assertEqual(person1.cv.extension, '.txt')
37+
self.assertTrue(exists(join(self.temp_path, person1.cv.path)))
38+
39+
person1.cv = resume, 'text/plain', 'myfile.note'
40+
self.assertEqual(person1.cv.content_type, 'text/plain')
41+
self.assertEqual(person1.cv.extension, '.note')
42+
self.assertTrue(exists(join(self.temp_path, person1.cv.path)))
43+
44+
45+
46+
if __name__ == '__main__': # pragma: no cover
47+
unittest.main()
48+

0 commit comments

Comments
 (0)