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
« 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
5class ProviderDetector:
6 """Detect provider type from repository path."""
8 @staticmethod
9 def detect(path: str, default: Optional[str] = None) -> str:
10 """
11 Detect provider from path format.
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)
22 Args:
23 path: Repository path to analyze
24 default: Default provider if detection is ambiguous
26 Returns:
27 Provider type: "gitlab", "github", "local", etc.
29 Raises:
30 ValueError: Cannot detect provider from path
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.
41 # Local filesystem paths
42 if path.startswith("/") or path.startswith("~"):
43 return "local"
44 if path.startswith("."):
45 return "local"
47 # Git-style paths
48 parts = path.split("/")
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 )
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"
64 # Three or more parts: group/subgroup/project (typically GitLab)
65 if len(parts) >= 3:
66 return "gitlab"
68 # Fallback to default or error
69 if default:
70 return default
72 raise ValueError(f"Cannot detect provider from path: {path}")
74 @staticmethod
75 def normalize_path(path: str, provider: str) -> str:
76 """
77 Normalize path for given provider.
79 Removes protocol prefixes and ensures correct format.
81 Args:
82 path: Repository path (may include protocol)
83 provider: Provider type
85 Returns:
86 Normalized path without protocol
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
98 return path
100 @staticmethod
101 def to_library_id(path: str, provider: str) -> str:
102 """
103 Convert path and provider to library_id URI format.
105 Args:
106 path: Repository path
107 provider: Provider type
109 Returns:
110 Library ID in URI format
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)
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}"
125 return f"{provider}://{normalized}"
127 @staticmethod
128 def from_library_id(library_id: str) -> tuple[str, str]:
129 """
130 Parse library_id URI into provider and path.
132 Args:
133 library_id: Library ID in URI format
135 Returns:
136 Tuple of (provider, path)
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")
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}")
149 provider, path = library_id.split("://", 1)
151 return provider, path