Skip to content

Commit

Permalink
pymel.conf: add deleted_pynode_name_access to change behavior when a …
Browse files Browse the repository at this point in the history
…deleted node is used

addresses #396
  • Loading branch information
pmolodo committed Jun 12, 2017
1 parent 5c14187 commit 44a2f38
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 4 deletions.
31 changes: 31 additions & 0 deletions pymel/core/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -2086,6 +2086,37 @@ def __str__(self):
msg += ": %r" % (self.node)
return msg

class DeletedMayaNodeError(MayaNodeError):
def __init__(self, node=None):
if hasattr(node, '_name'):
# Since the object has been deleted, normal name lookup for
# DependNode may not work
node = node._name
super(DeletedMayaNodeError, self).__init__(node=node)

def __str__(self):
if self.node:
# using this formatting for backwards compatibility
msg = "object %s no longer exists" % self.node
else:
msg = "object no longer exists"
return msg

@classmethod
def handle(cls, pynode):
option = _startup.pymel_options['deleted_pynode_name_access']
if option == 'ignore':
return
errorInst = cls(pynode)
if option == 'warn':
_logger.warn(str(errorInst))
elif option == 'error':
raise errorInst
else:
raise ValueError(
"unrecognized value for 'deleted_pynode_name_access': {}"
.format(option))

class MayaParticleAttributeError(MayaComponentError):
_objectDescription = 'Per-Particle Attribute'

Expand Down
23 changes: 20 additions & 3 deletions pymel/core/nodetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import pymel.internal.apicache as _apicache
import pymel.internal.pwarnings as _warnings
from pymel.internal import getLogger as _getLogger
from pymel.internal.startup import pymel_options as _pymel_options
import datatypes
_logger = _getLogger(__name__)

Expand Down Expand Up @@ -152,7 +153,7 @@ def name(self, update=True, stripNamespace=False, levels=0, long=False,
try:
self._updateName()
except general.MayaObjectError:
_logger.warn("object %s no longer exists" % self._name)
general.DeletedMayaNodeError.handle(self)
name = self._name
if stripNamespace:
if levels:
Expand Down Expand Up @@ -962,20 +963,36 @@ def name(self, update=True, long=False, stripNamespace=False, levels=0,
u'|imagePlane1|imagePlaneShape1'
'''
if update or long or self._name is None:
exists = True
if _pymel_options['deleted_pynode_name_access'] != 'ignore':
exists = self.exists()
try:
name = self._updateName(long)
# _updateName for dag nodes won't actually check if the object
# still exists... so we do that check ourselves. Also, even
# though we may already know the object doesn't exist, we still
# try to update the name... because having an updated name may
# be nice for any potential error / warning messages
if not exists:
raise general.MayaObjectError
except general.MayaObjectError:
# if we have an error, but we're only looking for the nodeName,
# use the non-dag version
if long is None and self.exists():
if long is None and exists:
# don't use DependNode._updateName, as that can still
# raise MayaInstanceError - want this to work, so people
# have a way to get the correct instance, assuming they know
# what the parent should be
name = _api.MFnDependencyNode(self.__apimobject__()).name()
else:
_logger.warn("object %s no longer exists" % self._name)
if not exists:
general.DeletedMayaNodeError.handle(self)
name = self._name
if name is None:
# if we've never gotten a name, but we're set to ignore
# deleted node errors, then just reraise the original
# error
raise
else:
name = self._name

Expand Down
1 change: 1 addition & 0 deletions pymel/internal/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ def parsePymelConfig():
# available
'fix_mayapy_segfault': 'on' if _hasUninitialize else 'off',
'fix_linux_mayapy_segfault': 'off' if _hasUninitialize else 'on',
'deleted_pynode_name_access': 'warn',
}

config = ConfigParser.ConfigParser(defaults)
Expand Down
17 changes: 17 additions & 0 deletions pymel/pymel.conf
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,20 @@ args=(os.path.expanduser('~/pymel.log'), 'w')
## used. If not set, it currently defaults to pyqt.
preferred_python_qt_binding = pyqt
#preferred_python_qt_binding = pyside

## Controls behavior when using deleted PyNodes
## Currently, the default behavior is "warn", which simply prints a warning when
## accessing the name of a deleted PyNode. Since most wrappers of mel commands
## work by first converting PyNodes to their names, then running the mel command
## on the string names, the net effect is that the mel command will work (with a
## warning if) a node with the same name exists in the scene, on that "other"
## similarly named node, but error if no node with that name exists in the
## scene anymore.
## Since this is somewhat confusing and likely unexpected behavior, we will
## likely be deprecating this at some point, with the aim to making the default
## "error"
## The valid options here are "ignore", "warn", and "error". Note that even if
## the setting is "ignore", this will only control what happens if you
## try to access a deleted PyNode's name - most operations will still error if
## no node with that name exists in the scene
deleted_pynode_name_access = warn
105 changes: 104 additions & 1 deletion tests/test_general.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import sys, os, inspect, unittest
import sys, os, inspect, unittest, logging
#from testingutils import setupUnittestModule
from pymel.core import *
import pymel.core as pm
import pymel.core.general
import pymel.versions as versions
import pymel.internal.factories as factories
import pymel.internal.pmcmds as pmcmds
Expand Down Expand Up @@ -2016,6 +2017,108 @@ def test_dyn_attribute(self):
cmds.delete(node.name())
self.assertFalse(attr.exists())


class CapturingHandler(logging.Handler):
'''Log handler that just records emitted messages'''
def __init__(self, *args, **kwargs):
super(CapturingHandler, self).__init__(*args, **kwargs)
self.capturedMessagesByLevel = {}

def emit(self, record):
messages = self.capturedMessagesByLevel.setdefault(record.levelno, [])
messages.append(record.msg)


class test_deletedNameAccess(unittest.TestCase):

class SetOptionAndCatchWarnings(object):
'''Temporarily set deleted_pynode_name_access, and catch warnings
emitted using the given logger'''
def __init__(self, newOption, logger=pymel.core.general._logger):
self.oldOption = None
self.newOption = newOption
self.logger = logger
self.capturingHandler = CapturingHandler(logging.DEBUG)
self.capturedWarnings = self.capturingHandler.capturedMessagesByLevel.setdefault(logging.WARN, [])
self.emittedWarnings = []

def __enter__(self):
from pymel.internal.startup import pymel_options
self.oldOption = pymel_options['deleted_pynode_name_access']
pymel_options['deleted_pynode_name_access'] = self.newOption
self.logger.addHandler(self.capturingHandler)
return self

def __exit__(self, exc_type, exc_val, exc_tb):
from pymel.internal.startup import pymel_options
pymel_options['deleted_pynode_name_access'] = self.oldOption
self.logger.removeHandler(self.capturingHandler)

def setUp(self):
cmds.file(new=1, f=1)
self.name = 'test_obj'
self.deleted_node = pm.createNode('transform', name=self.name)
pm.delete(self.deleted_node)
self.assertFalse(self.deleted_node.exists())

def test_ignore(self):
capturer = self.SetOptionAndCatchWarnings('ignore')
with capturer:
self.assertEqual(self.deleted_node.name(), self.name)
self.assertEqual(len(capturer.capturedWarnings), 0)

# make a joint with the same name
joint = pm.createNode('joint', name='test_obj')
self.assertTrue(joint.exists())

with capturer:
# delete the original, already-deleted, node
pm.delete(self.deleted_node)
self.assertEqual(len(capturer.capturedWarnings), 0)

# check that the joint was deleted
self.assertFalse(joint.exists())

def test_warn(self):
capturer = self.SetOptionAndCatchWarnings('warn')
with capturer:
self.assertEqual(self.deleted_node.name(), self.name)
self.assertEqual(len(capturer.capturedWarnings), 1)
self.assertIn('no longer exists', capturer.capturedWarnings[0])

# make a joint with the same name
joint = pm.createNode('joint', name='test_obj')
self.assertTrue(joint.exists())

with capturer:
# delete the original, already-deleted, node
pm.delete(self.deleted_node)
self.assertEqual(len(capturer.capturedWarnings), 2)
self.assertIn('no longer exists', capturer.capturedWarnings[1])

# check that the joint was deleted
self.assertFalse(joint.exists())

def test_error(self):
capturer = self.SetOptionAndCatchWarnings('error')
with capturer:
self.assertRaises(pymel.core.DeletedMayaNodeError,
self.deleted_node.name)
self.assertEqual(len(capturer.capturedWarnings), 0)

# make a joint with the same name
joint = pm.createNode('joint', name='test_obj')
self.assertTrue(joint.exists())

with capturer:
# delete the original, already-deleted, node
self.assertRaises(pymel.core.DeletedMayaNodeError,
pm.delete, self.deleted_node)
self.assertEqual(len(capturer.capturedWarnings), 0)

# check that the joint was NOT deleted
self.assertTrue(joint.exists())

#suite = unittest.TestLoader().loadTestsFromTestCase(testCase_nodesAndAttributes)
#suite.addTest(unittest.TestLoader().loadTestsFromTestCase(testCase_listHistory))
#unittest.TextTestRunner(verbosity=2).run(suite)
Expand Down

0 comments on commit 44a2f38

Please sign in to comment.