Repository: evennia/evennia

Issue:
SaverMutable will raise ValueError on operations if underlying Attribute removed
#### Brief summary of issue / Description of requested feature:
SaverLists, SaverDicts, etc, all store the underlying Attribute as its `._db_obj` attribute, and set the Attribute's .value appropriately upon assignment, allowing for dict and list operations to save their state without needing to be reassigned to the Attribute. I ran into a strange edge case where if you're not careful, you could get a confusing error. If the underlying Attribute is deleted while the mutable still exists, you'll see the following traceback:
```
Traceback (most recent call last):
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/evennia/evennia/commands/cmdhandler.py", line 584, in _run_command
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     ret = cmd.func()
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "./commands/commands/jobs.py", line 447, in func
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     invite_dict[targ] = ticket
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/evennia/evennia/utils/dbserialize.py", line 118, in save_wrapper
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     self._save_tree()
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/evennia/evennia/utils/dbserialize.py", line 145, in _save_tree
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     self._db_obj.value = self
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/evennia/evennia/typeclasses/attributes.py", line 152, in __value_set
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     self.save(update_fields=["db_value"])
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/evennia/evennia/utils/idmapper/models.py", line 382, in save
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     super(SharedMemoryModel, self).save(*args, **kwargs)
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/local/lib/python2.7/site-packages/django/db/models/base.py", line 708, in save
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     force_update=force_update, update_fields=update_fields)
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/local/lib/python2.7/site-packages/django/db/models/base.py", line 736, in save_base
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]   File "/home/tehom/arx/local/lib/python2.7/site-packages/django/db/models/base.py", line 792, in _save_table
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::]     raise ValueError("Cannot force an update in save() with no primary key.")
2017-08-18T16:07:17-0400 [AMPProtocol,0,127.0.0.1] [::] ValueError: Cannot force an update in save() with no primary key.
```


#### Steps to reproduce the issue / Reasons for adding feature:

1. my_dict = self.db.some_dict_attr
2. self.attributes.remove('some_dict_attr')
3. my_dict[some_key] = some_value

#### Error output / Expected result of feature
The above traceback.

#### Extra information, such as Evennia revision/repo/branch, operating system and ideas for how to solve / implement:
It's a wacky edge case that I only ran into when I was popping keys from a dictionary stored in an Attribute, removing the Attribute if the dict was empty, and then a player requested that I re-add the previous key on some failure condition. While it's easy to avoid, I was trying to think if there'd be some elegant way to have the SaverMutables handle this, but not sure if there's a good solution. Are the fields of a deleted object still accessible after it's been deleted? If so, maybe could create a new Attribute and save it.


Discussion:
If auto-restoring Attributes in SaverMutables is desirable behavior, we could clone a new Attribute with the same fields by setting its .pk to None and just calling `.save()` to create a new instance. The downside is that doesn't restore the ManyToMany relationship, leaving it orphaned. So what we could do is override `.delete()` in Attribute to temporarily store a reference to the object to allow a new Attribute to be constructed in what had it originally. 

Alternately, it might be cleaner just to raise a more clear error message in SaverMutable that informs them that they're modifying a deleted Attribute. It probably should be an error, but the current django error isn't super clear about what caused it.
Arguably, if you delete the Attribute but keep the mutable stored on it, you have in a very real way disconnected it from the database, similarly to when you do `disconnected_list = list(obj.db.mylist)`. I don't think the Attribute should be silently re-created behind the scenes (nor should the error be silenced); If the Attribute was deleted intentionally, it would be an error to keep modifying it without first recasting it into a non-Saver form with `list()`, `dict()` etc. Conversely, if it was unintentional then it is a bug that must be reported rather than silently fixed.

So I think raising a more consistent error is the way to go here, suggesting that since the Attribute was deleted, the mutable must be cast to a non-Saver form before it can be modified again (or explicitly re-added as a new Attribute).

Resolving patch:
diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py
--- a/evennia/utils/dbserialize.py
+++ b/evennia/utils/dbserialize.py
@@ -153,6 +153,12 @@ def _save_tree(self):
         if self._parent:
             self._parent._save_tree()
         elif self._db_obj:
+            if not self._db_obj.pk:
+                cls_name = self.__class__.__name__
+                non_saver_name = cls_name.lstrip("_Saver")
+                err_msg = "%s %s has had its root Attribute deleted." % (cls_name, self)
+                err_msg += " It must be cast to a %s before it can be modified further." % non_saver_name
+                raise ValueError(err_msg)
             self._db_obj.value = self
         else:
             logger.log_err("_SaverMutable %s has no root Attribute to save to." % self)


Test patch: