Skip to content

Commit 4e27c49

Browse files
committed
Better Provider Exceptions
1 parent 5a24d98 commit 4e27c49

File tree

5 files changed

+231
-77
lines changed

5 files changed

+231
-77
lines changed

cppython/plugins/conan/plugin.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from cppython.plugins.conan.builder import Builder
2121
from cppython.plugins.conan.resolution import resolve_conan_data, resolve_conan_dependency
2222
from cppython.plugins.conan.schema import ConanData
23-
from cppython.utility.exception import NotSupportedError
23+
from cppython.utility.exception import NotSupportedError, ProviderConfigurationError, ProviderInstallationError
2424
from cppython.utility.utility import TypeName
2525

2626

@@ -76,44 +76,49 @@ def _install_dependencies(self, *, update: bool = False) -> None:
7676
update: If True, check remotes for newer versions/revisions and install those.
7777
If False, use cached versions when available.
7878
"""
79-
resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies]
80-
81-
self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies)
82-
83-
self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True)
84-
85-
# Install/update dependencies using Conan API
86-
project_root = self.core_data.project_data.project_root
87-
conanfile_path = project_root / 'conanfile.py'
79+
try:
80+
resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies]
81+
82+
self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies)
83+
84+
self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True)
85+
86+
# Install/update dependencies using Conan API
87+
project_root = self.core_data.project_data.project_root
88+
conanfile_path = project_root / 'conanfile.py'
89+
90+
if conanfile_path.exists():
91+
# Initialize Conan API
92+
conan_api = ConanAPI()
93+
94+
# Get default profiles
95+
profile_host_path = conan_api.profiles.get_default_host()
96+
profile_build_path = conan_api.profiles.get_default_build()
97+
profile_host = conan_api.profiles.get_profile([profile_host_path])
98+
profile_build = conan_api.profiles.get_profile([profile_build_path])
99+
100+
# Build dependency graph for the package
101+
deps_graph = conan_api.graph.load_graph_consumer(
102+
path=str(conanfile_path),
103+
name=None,
104+
version=None,
105+
user=None,
106+
channel=None,
107+
profile_host=profile_host,
108+
profile_build=profile_build,
109+
lockfile=None,
110+
remotes=conan_api.remotes.list(),
111+
update=update,
112+
check_updates=update,
113+
is_build_require=False,
114+
)
88115

89-
if conanfile_path.exists():
90-
# Initialize Conan API
91-
conan_api = ConanAPI()
92-
93-
# Get default profiles
94-
profile_host_path = conan_api.profiles.get_default_host()
95-
profile_build_path = conan_api.profiles.get_default_build()
96-
profile_host = conan_api.profiles.get_profile([profile_host_path])
97-
profile_build = conan_api.profiles.get_profile([profile_build_path])
98-
99-
# Build dependency graph for the package
100-
deps_graph = conan_api.graph.load_graph_consumer(
101-
path=str(conanfile_path),
102-
name=None,
103-
version=None,
104-
user=None,
105-
channel=None,
106-
profile_host=profile_host,
107-
profile_build=profile_build,
108-
lockfile=None,
109-
remotes=conan_api.remotes.list(),
110-
update=update,
111-
check_updates=update,
112-
is_build_require=False,
113-
)
114-
115-
# Install dependencies
116-
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=conan_api.remotes.list())
116+
# Install dependencies
117+
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=conan_api.remotes.list())
118+
except Exception as e:
119+
operation = 'update' if update else 'install'
120+
error_msg = str(e)
121+
raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {error_msg}', e) from e
117122

118123
def install(self) -> None:
119124
"""Installs the provider"""
@@ -229,7 +234,7 @@ def publish(self) -> None:
229234
# Get the first configured remote or raise an error
230235
remotes = conan_api.remotes.list()
231236
if not remotes:
232-
raise RuntimeError('No remotes configured for upload')
237+
raise ProviderConfigurationError('conan', 'No remotes configured for upload', 'remotes')
233238

234239
remote = remotes[0] # Use first remote
235240

@@ -244,4 +249,4 @@ def publish(self) -> None:
244249
dry_run=False,
245250
)
246251
else:
247-
raise RuntimeError('No packages found to upload')
252+
raise ProviderInstallationError('conan', 'No packages found to upload')

cppython/plugins/vcpkg/plugin.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from cppython.plugins.cmake.schema import CMakeSyncData
1818
from cppython.plugins.vcpkg.resolution import generate_manifest, resolve_vcpkg_data
1919
from cppython.plugins.vcpkg.schema import VcpkgData
20-
from cppython.utility.exception import NotSupportedError
20+
from cppython.utility.exception import NotSupportedError, ProviderInstallationError, ProviderToolingError
2121
from cppython.utility.utility import TypeName
2222

2323

@@ -92,10 +92,9 @@ def _update_provider(cls, path: Path) -> None:
9292
capture_output=True,
9393
)
9494
except subprocess.CalledProcessError as e:
95-
logger.error(
96-
'Unable to bootstrap the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e), exc_info=True
97-
)
98-
raise
95+
error_msg = e.stderr.decode() if e.stderr else str(e)
96+
logger.error('Unable to bootstrap the vcpkg repository: %s', error_msg, exc_info=True)
97+
raise ProviderToolingError('vcpkg', 'bootstrap', error_msg, e) from e
9998

10099
def sync_data(self, consumer: SyncConsumer) -> SyncData:
101100
"""Gathers a data object for the given generator
@@ -167,8 +166,9 @@ async def download_tooling(cls, directory: Path) -> None:
167166
capture_output=True,
168167
)
169168
except subprocess.CalledProcessError as e:
170-
logger.exception('Unable to update the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e))
171-
raise
169+
error_msg = e.stderr.decode() if e.stderr else str(e)
170+
logger.error('Unable to update the vcpkg repository: %s', error_msg, exc_info=True)
171+
raise ProviderToolingError('vcpkg', 'update', error_msg, e) from e
172172
else:
173173
try:
174174
logger.debug("Cloning the vcpkg repository to '%s'", directory.absolute())
@@ -182,8 +182,9 @@ async def download_tooling(cls, directory: Path) -> None:
182182
)
183183

184184
except subprocess.CalledProcessError as e:
185-
logger.exception('Unable to clone the vcpkg repository: %s', e.stderr.decode() if e.stderr else str(e))
186-
raise
185+
error_msg = e.stderr.decode() if e.stderr else str(e)
186+
logger.error('Unable to clone the vcpkg repository: %s', error_msg, exc_info=True)
187+
raise ProviderToolingError('vcpkg', 'clone', error_msg, e) from e
187188

188189
cls._update_provider(directory)
189190

@@ -210,8 +211,9 @@ def install(self) -> None:
210211
capture_output=True,
211212
)
212213
except subprocess.CalledProcessError as e:
213-
logger.exception('Unable to install project dependencies: %s', e.stderr.decode() if e.stderr else str(e))
214-
raise
214+
error_msg = e.stderr.decode() if e.stderr else str(e)
215+
logger.error('Unable to install project dependencies: %s', error_msg, exc_info=True)
216+
raise ProviderInstallationError('vcpkg', error_msg, e) from e
215217

216218
def update(self) -> None:
217219
"""Called when dependencies need to be updated and written to the lock file."""
@@ -237,8 +239,9 @@ def update(self) -> None:
237239
capture_output=True,
238240
)
239241
except subprocess.CalledProcessError as e:
240-
logger.exception('Unable to install project dependencies: %s', e.stderr.decode() if e.stderr else str(e))
241-
raise
242+
error_msg = e.stderr.decode() if e.stderr else str(e)
243+
logger.error('Unable to update project dependencies: %s', error_msg, exc_info=True)
244+
raise ProviderInstallationError('vcpkg', error_msg, e) from e
242245

243246
def publish(self) -> None:
244247
"""Called when the project needs to be published.

cppython/project.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def install(self) -> None:
5656
"""Installs project dependencies
5757
5858
Raises:
59-
Exception: Raised if failed
59+
Exception: Provider-specific exceptions are propagated with full context
6060
"""
6161
if not self._enabled:
6262
self.logger.info('Skipping install because the project is not enabled')
@@ -68,19 +68,15 @@ def install(self) -> None:
6868
self.logger.info('Installing project')
6969
self.logger.info('Installing %s provider', self._data.plugins.provider.name())
7070

71-
try:
72-
self._data.plugins.provider.install()
73-
except Exception as exception:
74-
self.logger.error('Unexpected error during installation: %s', str(exception))
75-
raise SystemExit('Error: An unexpected error occurred during installation.') from None
76-
71+
# Let provider handle its own exceptions for better error context
72+
self._data.plugins.provider.install()
7773
self._data.sync()
7874

7975
def update(self) -> None:
8076
"""Updates project dependencies
8177
8278
Raises:
83-
Exception: Raised if failed
79+
Exception: Provider-specific exception
8480
"""
8581
if not self._enabled:
8682
self.logger.info('Skipping update because the project is not enabled')
@@ -92,18 +88,15 @@ def update(self) -> None:
9288
self.logger.info('Updating project')
9389
self.logger.info('Updating %s provider', self._data.plugins.provider.name())
9490

95-
try:
96-
self._data.plugins.provider.update()
97-
except Exception as exception:
98-
self.logger.error('Unexpected error during update: %s', str(exception))
99-
raise SystemExit('Error: An unexpected error occurred during update.') from None
100-
91+
# Let provider handle its own exceptions for better error context
92+
self._data.plugins.provider.update()
10193
self._data.sync()
10294

10395
def publish(self) -> None:
104-
"""Publishes the project"""
105-
try:
106-
self._data.plugins.provider.publish()
107-
except Exception as exception:
108-
self.logger.error('Unexpected error during publish: %s', str(exception))
109-
raise SystemExit('Error: An unexpected error occurred during publish.') from None
96+
"""Publishes the project
97+
98+
Raises:
99+
Exception: Provider-specific exception
100+
"""
101+
# Let provider handle its own exceptions for better error context
102+
self._data.plugins.provider.publish()

cppython/utility/exception.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,155 @@ def error(self) -> str:
4545
str -- The underlying error
4646
"""
4747
return self._error
48+
49+
50+
class ProviderInstallationError(Exception):
51+
"""Raised when provider installation fails"""
52+
53+
def __init__(self, provider_name: str, error: str, original_error: Exception | None = None) -> None:
54+
"""Initializes the error
55+
56+
Args:
57+
provider_name: The name of the provider that failed
58+
error: The error message
59+
original_error: The original exception that caused this error
60+
"""
61+
self._provider_name = provider_name
62+
self._error = error
63+
self._original_error = original_error
64+
65+
message = f"Provider '{provider_name}' installation failed: {error}"
66+
super().__init__(message)
67+
68+
@property
69+
def provider_name(self) -> str:
70+
"""Returns the provider name
71+
72+
Returns:
73+
str -- The provider name
74+
"""
75+
return self._provider_name
76+
77+
@property
78+
def error(self) -> str:
79+
"""Returns the underlying error
80+
81+
Returns:
82+
str -- The underlying error
83+
"""
84+
return self._error
85+
86+
@property
87+
def original_error(self) -> Exception | None:
88+
"""Returns the original exception
89+
90+
Returns:
91+
Exception | None -- The original exception if available
92+
"""
93+
return self._original_error
94+
95+
96+
class ProviderConfigurationError(Exception):
97+
"""Raised when provider configuration is invalid"""
98+
99+
def __init__(self, provider_name: str, error: str, configuration_key: str | None = None) -> None:
100+
"""Initializes the error
101+
102+
Args:
103+
provider_name: The name of the provider with invalid configuration
104+
error: The error message
105+
configuration_key: The specific configuration key that caused the error
106+
"""
107+
self._provider_name = provider_name
108+
self._error = error
109+
self._configuration_key = configuration_key
110+
111+
message = f"Provider '{provider_name}' configuration error"
112+
if configuration_key:
113+
message += f" in '{configuration_key}'"
114+
message += f': {error}'
115+
super().__init__(message)
116+
117+
@property
118+
def provider_name(self) -> str:
119+
"""Returns the provider name
120+
121+
Returns:
122+
str -- The provider name
123+
"""
124+
return self._provider_name
125+
126+
@property
127+
def error(self) -> str:
128+
"""Returns the underlying error
129+
130+
Returns:
131+
str -- The underlying error
132+
"""
133+
return self._error
134+
135+
@property
136+
def configuration_key(self) -> str | None:
137+
"""Returns the configuration key
138+
139+
Returns:
140+
str | None -- The configuration key if available
141+
"""
142+
return self._configuration_key
143+
144+
145+
class ProviderToolingError(Exception):
146+
"""Raised when provider tooling operations fail"""
147+
148+
def __init__(self, provider_name: str, operation: str, error: str, original_error: Exception | None = None) -> None:
149+
"""Initializes the error
150+
151+
Args:
152+
provider_name: The name of the provider that failed
153+
operation: The operation that failed (e.g., 'download', 'bootstrap', 'install')
154+
error: The error message
155+
original_error: The original exception that caused this error
156+
"""
157+
self._provider_name = provider_name
158+
self._operation = operation
159+
self._error = error
160+
self._original_error = original_error
161+
162+
message = f"Provider '{provider_name}' {operation} failed: {error}"
163+
super().__init__(message)
164+
165+
@property
166+
def provider_name(self) -> str:
167+
"""Returns the provider name
168+
169+
Returns:
170+
str -- The provider name
171+
"""
172+
return self._provider_name
173+
174+
@property
175+
def operation(self) -> str:
176+
"""Returns the operation that failed
177+
178+
Returns:
179+
str -- The operation name
180+
"""
181+
return self._operation
182+
183+
@property
184+
def error(self) -> str:
185+
"""Returns the underlying error
186+
187+
Returns:
188+
str -- The underlying error
189+
"""
190+
return self._error
191+
192+
@property
193+
def original_error(self) -> Exception | None:
194+
"""Returns the original exception
195+
196+
Returns:
197+
Exception | None -- The original exception if available
198+
"""
199+
return self._original_error

0 commit comments

Comments
 (0)