@@ -454,38 +454,109 @@ def unstub(*objs):
454454 unstub("os.path.exists")
455455 unstub(cat.meow)
456456
457- In these cases only that one attribute is restored, while other stubs on
458- the same object stay active.
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.
459465
460466 Note that additionally, the underlying registry will be cleaned.
461467 After an `unstub` you can't :func:`verify` anymore because all
462468 interactions will be forgotten.
463469 """
464470
465- if objs :
466- for obj in objs :
467- if isinstance (obj , str ):
468- obj = get_obj (obj )
469-
470- # mock_registry.unstub(obj)
471- # patcher.unstub_matching(obj)
472- if (
473- mock_registry .unstub (obj )
474- or patcher .unstub_matching (obj )
475- ):
476- return
477-
478- resolved_target = _resolve_unstub_attr_target (obj )
479- if resolved_target is None :
480- continue
481-
482- host , attr_name = resolved_target
483- host_mock = mock_registry .mock_for (host )
484- if host_mock is not None :
485- host_mock .unstub_method (attr_name )
486- else :
471+ if not objs :
487472 mock_registry .unstub_all ()
488473 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+
489560
490561
491562def _resolve_unstub_attr_target (target ):
0 commit comments