@@ -543,12 +543,22 @@ def _ignore_patterns(path, names):
543543 return _ignore_patterns
544544
545545def _copytree (entries , src , dst , symlinks , ignore , copy_function ,
546- ignore_dangling_symlinks , dirs_exist_ok = False ):
546+ ignore_dangling_symlinks , dirs_exist_ok = False , _seen = None ):
547547 if ignore is not None :
548548 ignored_names = ignore (os .fspath (src ), [x .name for x in entries ])
549549 else :
550550 ignored_names = ()
551551
552+ # Track visited directories to detect cycles (e.g., Windows junctions)
553+ if _seen is None :
554+ _seen = set ()
555+ src_st = os .stat (src )
556+ src_id = (src_st .st_dev , src_st .st_ino )
557+ if src_id in _seen :
558+ raise Error ([(src , dst , "Infinite recursion detected" )])
559+ _seen = _seen .copy ()
560+ _seen .add (src_id )
561+
552562 os .makedirs (dst , exist_ok = dirs_exist_ok )
553563 errors = []
554564 use_srcentry = copy_function is copy2 or copy_function is copy
@@ -583,12 +593,12 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
583593 if srcentry .is_dir ():
584594 copytree (srcobj , dstname , symlinks , ignore ,
585595 copy_function , ignore_dangling_symlinks ,
586- dirs_exist_ok )
596+ dirs_exist_ok , _seen = _seen )
587597 else :
588598 copy_function (srcobj , dstname )
589599 elif srcentry .is_dir ():
590600 copytree (srcobj , dstname , symlinks , ignore , copy_function ,
591- ignore_dangling_symlinks , dirs_exist_ok )
601+ ignore_dangling_symlinks , dirs_exist_ok , _seen = _seen )
592602 else :
593603 # Will raise a SpecialFileError for unsupported file types
594604 copy_function (srcobj , dstname )
@@ -609,7 +619,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
609619 return dst
610620
611621def copytree (src , dst , symlinks = False , ignore = None , copy_function = copy2 ,
612- ignore_dangling_symlinks = False , dirs_exist_ok = False ):
622+ ignore_dangling_symlinks = False , dirs_exist_ok = False , _seen = None ):
613623 """Recursively copy a directory tree and return the destination directory.
614624
615625 If exception(s) occur, an Error is raised with a list of reasons.
@@ -654,7 +664,7 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
654664 return _copytree (entries = entries , src = src , dst = dst , symlinks = symlinks ,
655665 ignore = ignore , copy_function = copy_function ,
656666 ignore_dangling_symlinks = ignore_dangling_symlinks ,
657- dirs_exist_ok = dirs_exist_ok )
667+ dirs_exist_ok = dirs_exist_ok , _seen = _seen )
658668
659669if hasattr (os .stat_result , 'st_file_attributes' ):
660670 def _rmtree_islink (st ):
0 commit comments