Coverage for repo_ctx / providers / detector.py: 93%

43 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-25 17:42 +0100

1"""Provider detection from repository paths.""" 

2from typing import Optional 

3 

4 

5class ProviderDetector: 

6 """Detect provider type from repository path.""" 

7 

8 @staticmethod 

9 def detect(path: str, default: Optional[str] = None) -> str: 

10 """ 

11 Detect provider from path format. 

12 

13 Examples: 

14 "owner/repo" -> "github" (2 parts, typical GitHub format) 

15 "group/subgroup/project" -> "gitlab" (3+ parts) 

16 "/absolute/path" -> "local" 

17 "./relative/path" -> "local" 

18 "../path" -> "local" 

19 "~/path" -> "local" 

20 "pypi:package" -> "pypi" (future) 

21 

22 Args: 

23 path: Repository path to analyze 

24 default: Default provider if detection is ambiguous 

25 

26 Returns: 

27 Provider type: "gitlab", "github", "local", etc. 

28 

29 Raises: 

30 ValueError: Cannot detect provider from path 

31 

32 Note: 

33 For ambiguous cases (owner/repo could be GitLab or GitHub), 

34 the default provider is used if provided, otherwise "github" is assumed. 

35 """ 

36 # Handle explicit protocol prefixes 

37 if "://" in path: 

38 protocol = path.split("://")[0] 

39 return protocol # gitlab://, github://, local://, etc. 

40 

41 # Local filesystem paths 

42 if path.startswith("/") or path.startswith("~"): 

43 return "local" 

44 if path.startswith("."): 

45 return "local" 

46 

47 # Git-style paths 

48 parts = path.split("/") 

49 

50 # Single part is invalid 

51 if len(parts) == 1: 

52 raise ValueError( 

53 f"Cannot detect provider from path: {path}. " 

54 f"Expected format: owner/repo, group/project, or /path/to/repo" 

55 ) 

56 

57 # Two parts: owner/repo (typically GitHub) 

58 if len(parts) == 2: 

59 # Could be GitLab or GitHub, use default or assume GitHub 

60 if default: 

61 return default 

62 return "github" 

63 

64 # Three or more parts: group/subgroup/project (typically GitLab) 

65 if len(parts) >= 3: 

66 return "gitlab" 

67 

68 # Fallback to default or error 

69 if default: 

70 return default 

71 

72 raise ValueError(f"Cannot detect provider from path: {path}") 

73 

74 @staticmethod 

75 def normalize_path(path: str, provider: str) -> str: 

76 """ 

77 Normalize path for given provider. 

78 

79 Removes protocol prefixes and ensures correct format. 

80 

81 Args: 

82 path: Repository path (may include protocol) 

83 provider: Provider type 

84 

85 Returns: 

86 Normalized path without protocol 

87 

88 Examples: 

89 gitlab://group/project -> group/project 

90 github://owner/repo -> owner/repo 

91 local:///path/to/repo -> /path/to/repo 

92 """ 

93 # Remove protocol if present 

94 if "://" in path: 

95 protocol, rest = path.split("://", 1) 

96 return rest 

97 

98 return path 

99 

100 @staticmethod 

101 def to_library_id(path: str, provider: str) -> str: 

102 """ 

103 Convert path and provider to library_id URI format. 

104 

105 Args: 

106 path: Repository path 

107 provider: Provider type 

108 

109 Returns: 

110 Library ID in URI format 

111 

112 Examples: 

113 ("group/project", "gitlab") -> "gitlab://group/project" 

114 ("owner/repo", "github") -> "github://owner/repo" 

115 ("/path/to/repo", "local") -> "local:///path/to/repo" 

116 """ 

117 normalized = ProviderDetector.normalize_path(path, provider) 

118 

119 if provider == "local": 

120 # Ensure absolute path starts with / 

121 if not normalized.startswith("/"): 

122 raise ValueError(f"Local paths must be absolute: {normalized}") 

123 return f"local://{normalized}" 

124 

125 return f"{provider}://{normalized}" 

126 

127 @staticmethod 

128 def from_library_id(library_id: str) -> tuple[str, str]: 

129 """ 

130 Parse library_id URI into provider and path. 

131 

132 Args: 

133 library_id: Library ID in URI format 

134 

135 Returns: 

136 Tuple of (provider, path) 

137 

138 Examples: 

139 "gitlab://group/project" -> ("gitlab", "group/project") 

140 "github://owner/repo" -> ("github", "owner/repo") 

141 "local:///path/to/repo" -> ("local", "/path/to/repo") 

142 

143 Raises: 

144 ValueError: Invalid library_id format 

145 """ 

146 if "://" not in library_id: 

147 raise ValueError(f"Invalid library_id format: {library_id}") 

148 

149 provider, path = library_id.split("://", 1) 

150 

151 return provider, path