@@ -448,20 +448,142 @@ def unstub(*objs):
448448 If you don't pass in any argument, *all* registered mocks and
449449 patched modules, classes etc. will be unstubbed.
450450
451+ You can also unstub a single method/function target, e.g.::
452+
453+ unstub(os.path.exists)
454+ unstub("os.path.exists")
455+ unstub(cat.meow)
456+
457+ Or explicitly target one attribute by host and name, e.g.::
458+
459+ unstub((cat, "meow"))
460+ unstub(cat, "meow")
461+ unstub((cat, "meow"), (os.path, "exists"))
462+
463+ In these cases only the selected attributes are restored, while other stubs
464+ on the same objects stay active.
465+
451466 Note that additionally, the underlying registry will be cleaned.
452467 After an `unstub` you can't :func:`verify` anymore because all
453468 interactions will be forgotten.
454469 """
455470
456- if objs :
457- for obj in objs :
458- if isinstance (obj , str ):
459- obj = get_obj (obj )
460- mock_registry .unstub (obj )
461- patcher .unstub_matching (obj )
462- else :
471+ if not objs :
463472 mock_registry .unstub_all ()
464473 patcher .unstub_all ()
474+ return
475+
476+ explicit_attr_targets , generic_targets = _partition_unstub_targets (objs )
477+
478+ for host , attr_name in explicit_attr_targets :
479+ _unstub_attr_target (host , attr_name )
480+
481+ for obj in generic_targets :
482+ if isinstance (obj , str ):
483+ obj = get_obj (obj )
484+
485+ if mock_registry .unstub (obj ) or patcher .unstub_matching (obj ):
486+ continue
487+
488+ resolved_target = _resolve_unstub_attr_target (obj )
489+ if resolved_target is None :
490+ continue
491+
492+ host , attr_name = resolved_target
493+ _unstub_attr_target (host , attr_name )
494+
495+
496+
497+ def _partition_unstub_targets (objs ):
498+ if _is_unstub_attr_pair_arguments (objs ):
499+ host , attr_name = objs
500+ return [
501+ _normalize_unstub_attr_target (host , attr_name )
502+ ], []
503+
504+ explicit_attr_targets = []
505+ generic_targets = []
506+
507+ for obj in objs :
508+ explicit_attr_target = _coerce_unstub_attr_target_tuple (obj )
509+ if explicit_attr_target is None :
510+ generic_targets .append (obj )
511+ continue
512+
513+ explicit_attr_targets .append (explicit_attr_target )
514+
515+ return explicit_attr_targets , generic_targets
516+
517+
518+
519+ def _is_unstub_attr_pair_arguments (objs ):
520+ return (
521+ len (objs ) == 2
522+ and not isinstance (objs [0 ], tuple )
523+ and _looks_like_attr_name (objs [1 ])
524+ )
525+
526+
527+
528+ def _coerce_unstub_attr_target_tuple (target ):
529+ if not isinstance (target , tuple ) or len (target ) != 2 :
530+ return None
531+
532+ host , attr_name = target
533+ if not _looks_like_attr_name (attr_name ):
534+ return None
535+
536+ return _normalize_unstub_attr_target (host , attr_name )
537+
538+
539+
540+ def _normalize_unstub_attr_target (host , attr_name ):
541+ if isinstance (host , str ):
542+ host = get_obj (host )
543+
544+ return host , attr_name
545+
546+
547+
548+ def _looks_like_attr_name (value ):
549+ return isinstance (value , str ) and bool (value ) and "." not in value
550+
551+
552+
553+ def _unstub_attr_target (host , attr_name ):
554+ host_mock = mock_registry .mock_for (host )
555+ if host_mock is not None :
556+ host_mock .unstub_method (attr_name )
557+
558+ patcher .unstub_attribute (host , attr_name )
559+
560+
561+
562+ def _resolve_unstub_attr_target (target ):
563+ if not callable (target ):
564+ return None
565+
566+ host = getattr (target , "__self__" , None )
567+ attr_name = getattr (target , "__name__" , None )
568+ if host is not None and attr_name is not None :
569+ return host , attr_name
570+
571+ target_function = _unwrap_unstub_target (target )
572+ for theMock in mock_registry .get_registered_mocks ():
573+ for method_name , patch in theMock ._methods_to_unstub .items ():
574+ replacement = getattr (patch , "replacement" , None )
575+ if _unwrap_unstub_target (replacement ) is target_function :
576+ return theMock .mocked_obj , method_name
577+
578+ return None
579+
580+
581+
582+ def _unwrap_unstub_target (target ):
583+ if isinstance (target , (staticmethod , classmethod )):
584+ return target .__func__
585+
586+ return getattr (target , "__func__" , target )
465587
466588
467589def forget_invocations (* objs ):
0 commit comments