Coverage for src / library_skills / python_env.py: 100%

94 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-03 20:19 +0000

1import os 2tcucvcwcxcyczcAcBc

2from pathlib import Path 2tcucvcwcxcyczcAcBc

3 

4from .workspace import find_node_workspace, find_uv_workspace 2tcucvcwcxcyczcAcBc

5 

6 

7def find_project_root(cwd: Path) -> Path | None: 2tcucvcwcxcyczcAcBc

8 """Find the nearest Python project/environment root walking up from cwd.""" 

9 workspace = find_uv_workspace(cwd) 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abCcDcTbb Yb|bic}b*bUb_b[bPcbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbEcFcVbc Zb~bjcac?bWb`b]bQcNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M lcmcRba Xb{bkcbc8bSb^b@bRcPbQb

10 if workspace is not None: 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abCcDcTbb Yb|bic}b*bUb_b[bPcbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbEcFcVbc Zb~bjcac?bWb`b]bQcNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M lcmcRba Xb{bkcbc8bSb^b@bRcPbQb

11 return workspace.root 29b!b%b'b(b)bic}bYcZc+b,b/b:b;b=bjcac0c1c0b1b4b5b6b7bkcbc2c3c

12 node_workspace = find_node_workspace(cwd) 22 3 4 5 #b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| } ~ abCcDcTbb Yb|b*bUb_b[bPcbbcbdbebfbgb-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJbKbLbMbEcFcVbc Zb~b?bWb`b]bQcNbObd e f g 2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ K L M lcmcRba Xb{b8bSb^b@bRcPbQb

13 if node_workspace is not None: 22 3 4 5 #b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| } ~ abCcDcTbb Yb|b*bUb_b[bPcbbcbdbebfbgb-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJbKbLbMbEcFcVbc Zb~b?bWb`b]bQcNbObd e f g 2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ K L M lcmcRba Xb{b8bSb^b@bRcPbQb

14 return node_workspace.root 2N O P Q R PcccdcS T U V W QcecfcX Y Z 0 1 Rcgchc

15 for directory in [cwd, *cwd.parents]: 22 3 4 5 #b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { $b| } ~ abCcDcTbb Yb|b*bUb_b[bbb)ccb*cdbebfbgb-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIb.bJbKbLbMbEcFcVbc Zb~b?bWb`b]bNb+cOb,cd e f g 2bh i j k l m n o p q r s t u v w x y z A B C D E F G H I 3bJ K L M lcmcRba Xb{b8bSb^b@bPb-cQb.c

16 if ( 2d e f g 2bh i j k l m n o p q r s t u v w x y z A B C D E F G H I 3bJ K L M lcmcRba Xb{b8bSb^b@b

17 (directory / "pyproject.toml").is_file() 

18 or (directory / "uv.lock").is_file() 

19 or _venv_from_dot_venv(directory) is not None 

20 or (directory / "package.json").is_file() 

21 or (directory / "node_modules").is_dir() 

22 ): 

23 return directory 22 3 4 5 6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | } ~ abCcDc|b*bUb[bbbcbdbebfbgbhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJbKbLbMbEcFc~b?bWb]bNbObd e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M lcmc{b8bSb@bPbQb

24 return None 2#b$bTbb Yb_b)c*c-b.bVbc Zb`b+c,c2b3bRba Xb^b-c.c

25 

26 

27def find_venv(cwd: Path | None = None) -> Path | None: 2tcucvcwcxcyczcAcBc

28 """Find the target Python environment. 

29 

30 The CLI can run from an isolated tool environment (for example via uvx), so 

31 prefer project-local environment signals before accepting an active shell env. 

32 

33 Resolution order: 

34 1. UV_PROJECT_ENVIRONMENT env var 

35 2. Nearest project .venv 

36 3. VIRTUAL_ENV if it appears to belong to the project/user cwd 

37 4. CONDA_PREFIX env var 

38 """ 

39 cwd = cwd or Path.cwd() 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abTbb Yb|bic}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbVbc Zb~bjcac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M Rba Xb{bkcbc8bSb^b@bPbQb

40 workspace = find_uv_workspace(cwd) 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abTbb Yb|bic}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbVbc Zb~bjcac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M Rba Xb{bkcbc8bSb^b@bPbQb

41 project_root = find_project_root(cwd) or cwd 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abTbb Yb|bic}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbVbc Zb~bjcac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M Rba Xb{bkcbc8bSb^b@bPbQb

42 

43 # 1. UV_PROJECT_ENVIRONMENT 

44 uv_proj_env = os.environ.get("UV_PROJECT_ENVIRONMENT") 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abTbb Yb|bic}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbVbc Zb~bjcac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M Rba Xb{bkcbc8bSb^b@bPbQb

45 if uv_proj_env: 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abTbb Yb|bic}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbVbc Zb~bjcac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M Rba Xb{bkcbc8bSb^b@bPbQb

46 path = Path(uv_proj_env) 2|bicScTc~bjcUcVc{bkcWcXc

47 if not path.is_absolute(): 2|bicScTc~bjcUcVc{bkcWcXc

48 path = project_root / path 2|bicScTc~bjcUcVc{bkcWcXc

49 if (path / "pyvenv.cfg").is_file(): 2|bicScTc~bjcUcVc{bkcWcXc

50 return path 2|bicScTc~bjcUcVc{bkcWcXc

51 

52 # 2. Nearest project .venv directory or PEP 832 redirect file 

53 if workspace is not None: 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abTbb Yb}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbVbc Zbac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M Rba Xbbc8bSb^b@bPbQb

54 venv = _venv_from_dot_venv(workspace.root) 29b!b%b'b(b)b}bYcZc+b,b/b:b;b=bac0c1c0b1b4b5b6b7bbc2c3c

55 if venv is not None: 29b!b%b'b(b)b}bYcZc+b,b/b:b;b=bac0c1c0b1b4b5b6b7bbc2c3c

56 return venv 29b!b%b'b(b)bYcZc+b,b/b:b;b=b0c1c0b1b4b5b6b7b2c3c

57 return None 2}bjdkdacldmdbcndod

58 

59 for directory in [cwd, *cwd.parents]: 22 3 4 5 #b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| } ~ abTbb Yb*bUb_b[bccbbdccbdbebfbgb-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJbKbLbMbVbc Zb?bWb`b]becNbfcObd e f g 2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ K L M Rba Xb8bSb^b@bgcPbhcQb

60 venv = _venv_from_dot_venv(directory) 22 3 4 5 #b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| } ~ abTbb Yb*bUb_b[bbbcbdbebfbgb-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJbKbLbMbVbc Zb?bWb`b]bNbObd e f g 2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ K L M Rba Xb8bSb^b@bPbQb

61 if venv is not None: 22 3 4 5 #b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| } ~ abTbb Yb*bUb_b[bbbcbdbebfbgb-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJbKbLbMbVbc Zb?bWb`b]bNbObd e f g 2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ K L M Rba Xb8bSb^b@bPbQb

62 return venv 22 3 4 5 6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | } ~ ab*bUbbbcbdbebfbgbhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJbKbLbMb?bWbNbObd e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M 8bSbPbQb

63 

64 # 3. VIRTUAL_ENV, but avoid uvx/tool environments outside the project tree 

65 virtual_env = os.environ.get("VIRTUAL_ENV") 2#bN O P Q R $bTbb Yb_b[bccdc-bS T U V W .bVbc Zb`b]becfc2bX Y Z 0 1 3bRba Xb^b@bgchc

66 if virtual_env: 2#bN O P Q R $bTbb Yb_b[bccdc-bS T U V W .bVbc Zb`b]becfc2bX Y Z 0 1 3bRba Xb^b@bgchc

67 path = Path(virtual_env) 2N O P Q R Yb[bccdcS T U V W Zb]becfcX Y Z 0 1 Xb@bgchc

68 if (path / "pyvenv.cfg").is_file() and _is_relative_to(path, project_root): 2N O P Q R Yb[bccdcS T U V W Zb]becfcX Y Z 0 1 Xb@bgchc

69 return path 2[b/c:c]b;c=c@b?c@c

70 

71 # 4. CONDA_PREFIX 

72 conda_prefix = os.environ.get("CONDA_PREFIX") 2#bN O P Q R $bTbb Yb_bccdc-bS T U V W .bVbc Zb`becfc2bX Y Z 0 1 3bRba Xb^bgchc

73 if conda_prefix: 2#bN O P Q R $bTbb Yb_bccdc-bS T U V W .bVbc Zb`becfc2bX Y Z 0 1 3bRba Xb^bgchc

74 path = Path(conda_prefix) 2_b4c5c`b6c7c^b8c9c

75 if path.is_dir(): 2_b4c5c`b6c7c^b8c9c

76 return path 2_b4c5c`b6c7c^b8c9c

77 

78 return None 2#bN O P Q R $bTbb Ybccdc-bS T U V W .bVbc Zbecfc2bX Y Z 0 1 3bRba Xbgchc

79 

80 

81def get_site_packages_dir(venv_path: Path) -> Path | None: 2tcucvcwcxcyczcAcBc

82 """Get the site-packages directory for a Python environment.""" 

83 windows_site_packages = venv_path / "Lib" / "site-packages" 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abKcHcbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbLcIcNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGcPbQb

84 if windows_site_packages.is_dir(): 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abKcHcbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbLcIcNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGcPbQb

85 return windows_site_packages 2Hc[c]cIc^c_cGc`c{c

86 

87 # Unix: lib/pythonX.Y/site-packages or lib64/pythonX.Y/site-packages 

88 for lib_name in ("lib", "lib64"): 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abKcHcbb[ccb]cdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbLcIcNb^cOb_cd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGcPb`cQb{c

89 lib_dir = venv_path / lib_name 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abKcHcbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbLcIcNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGcPbQb

90 if lib_dir.is_dir(): 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abKcHcbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbLcIcNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGcPbQb

91 for child in sorted(lib_dir.iterdir(), reverse=True): 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abKcHcbb|ccb}cdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbLcIcNb~cObadd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGcPbbdQbcd

92 site_packages = child / "site-packages" 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abKcHcbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbLcIcNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGcPbQb

93 if ( 2d e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M JcGc

94 child.name.startswith("python") 

95 and child.is_dir() 

96 and site_packages.is_dir() 

97 ): 

98 return site_packages 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abHcbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbIcNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M GcPbQb

99 

100 return None 2Kc|c}cLc~cadJcbdcd

101 

102 

103def find_node_modules(cwd: Path | None = None) -> Path | None: 2tcucvcwcxcyczcAcBc

104 """Find the nearest Node.js node_modules directory walking up from cwd.""" 

105 cwd = cwd or Path.cwd() 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abMcbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbNcNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M OcPbQb

106 for directory in [cwd, *cwd.parents]: 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abMcbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbNcNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M OcPbQb

107 node_modules = directory / "node_modules" 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abMcbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbNcNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M OcPbQb

108 if node_modules.is_dir(): 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abMcbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbNcNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M OcPbQb

109 return node_modules 29b!bN O P Q R { Mcpdqd+b,bS T U V W IbNcrdsd0b1bX Y Z 0 1 I Octdud

110 return None 22 3 4 5 #b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` $b| %b'b(b)b} ~ abMcbbcbdbebfbgb-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHb.bJb/b:b;b=bKbLbMbNcNbObd e f g 2bh i j k l m n o p q r s t u v w x y z A B C D E F G H 3bJ 4b5b6b7bK L M OcPbQb

111 

112 

113def _is_relative_to(path: Path, parent: Path) -> bool: 2tcucvcwcxcyczcAcBc

114 try: 2N O P Q R Yb[bccdcS T U V W Zb]becfcX Y Z 0 1 Xb@bgchc

115 path.resolve().relative_to(parent.resolve()) 2N O P Q R Yb[bccdcS T U V W Zb]becfcX Y Z 0 1 Xb@bgchc

116 except ValueError: 2N O P Q R YbccdcS T U V W ZbecfcX Y Z 0 1 Xbgchc

117 return False 2N O P Q R YbccdcS T U V W ZbecfcX Y Z 0 1 Xbgchc

118 return True 2[b/c:c]b;c=c@b?c@c

119 

120 

121def _venv_from_dot_venv(project_root: Path) -> Path | None: 2tcucvcwcxcyczcAcBc

122 dot_venv = project_root / ".venv" 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abCcDcTbb Yb}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbEcFcVbc Zbac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M lcmcRba Xbbc8bSb^b@bPbQb

123 if dot_venv.is_dir(): 22 3 4 5 9b!b#b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? N O P Q R @ [ ] ^ _ ` { $b| %b'b(b)b} ~ abCcDcTbb Yb}b*bUb_b[bbbcbdbebfbgb+b,b-bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbS T U V W CbDbEbFbGbHbIb.bJb/b:b;b=bKbLbMbEcFcVbc Zbac?bWb`b]bNbObd e f g 0b1b2bh i j k l m n o p q r s t u v w x y z A B X Y Z 0 1 C D E F G H I 3bJ 4b5b6b7bK L M lcmcRba Xbbc8bSb^b@bPbQb

124 if (dot_venv / "pyvenv.cfg").is_file(): 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abTbbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbVbNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M RbPbQb

125 return dot_venv 22 3 4 5 9b!b6 7 8 9 ! # $ % ' ( ) * + , - . / : ; = ? @ [ ] ^ _ ` { | %b'b(b)b} ~ abbbcbdbebfbgb+b,bhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbybzbAbBbCbDbEbFbGbHbIbJb/b:b;b=bKbLbMbNbObd e f g 0b1bh i j k l m n o p q r s t u v w x y z A B C D E F G H I J 4b5b6b7bK L M PbQb

126 return None 2Tb!c#cVb$c%cRb'c(c

127 if dot_venv.is_file(): 2#bN O P Q R $bCcDcTbb Yb}b*bUb_b[bccdc-bS T U V W .bEcFcVbc Zbac?bWb`b]becfc2bX Y Z 0 1 3blcmcRba Xbbc8bSb^b@bgchc

128 return _read_venv_redirect_file(dot_venv) 2Tbb *bUbncocVbc ?bWbpcqcRba 8bSbrcsc

129 return None 2#bN O P Q R $bCcDcTbb Yb}bUb_b[bccdc-bS T U V W .bEcFcVbc ZbacWb`b]becfc2bX Y Z 0 1 3blcmcRba XbbcSb^b@bgchc

130 

131 

132def _read_venv_redirect_file(path: Path) -> Path | None: 2tcucvcwcxcyczcAcBc

133 try: 2Tbb *bUbncocVbc ?bWbpcqcRba 8bSbrcsc

134 content = path.read_text(encoding="utf-8") 2Tbb *bUbncocVbc ?bWbpcqcRba 8bSbrcsc

135 except OSError: 2Tb!c#cVb$c%cRb'c(c

136 return None 2Tb!c#cVb$c%cRb'c(c

137 redirect = content.splitlines()[0] if content.splitlines() else "" 2b *bUbncocc ?bWbpcqca 8bSbrcsc

138 if not redirect: 2b *bUbncocc ?bWbpcqca 8bSbrcsc

139 return None 2b ddedc fdgda hdid

140 venv = Path(redirect) 2b *bUbncocc ?bWbpcqca 8bSbrcsc

141 if not venv.is_absolute(): 2b *bUbncocc ?bWbpcqca 8bSbrcsc

142 venv = path.parent / venv 2b Ubncocc Wbpcqca Sbrcsc

143 venv = venv.resolve() 2b *bUbncocc ?bWbpcqca 8bSbrcsc

144 if (venv / "pyvenv.cfg").is_file(): 2b *bUbncocc ?bWbpcqca 8bSbrcsc

145 return venv 2*bUbncoc?bWbpcqc8bSbrcsc

146 return None 2b ddedc fdgda hdid