qnty 0.0.9__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. qnty/__init__.py +2 -3
  2. qnty/constants/__init__.py +10 -0
  3. qnty/constants/numerical.py +18 -0
  4. qnty/constants/solvers.py +6 -0
  5. qnty/constants/tests.py +6 -0
  6. qnty/dimensions/__init__.py +23 -0
  7. qnty/dimensions/base.py +97 -0
  8. qnty/dimensions/field_dims.py +126 -0
  9. qnty/dimensions/field_dims.pyi +128 -0
  10. qnty/dimensions/signature.py +111 -0
  11. qnty/equations/__init__.py +1 -1
  12. qnty/equations/equation.py +118 -155
  13. qnty/equations/system.py +68 -65
  14. qnty/expressions/__init__.py +25 -46
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +46 -68
  17. qnty/expressions/nodes.py +540 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1101 -0
  21. qnty/problems/problem.py +737 -0
  22. qnty/problems/rules.py +145 -0
  23. qnty/problems/solving.py +1216 -0
  24. qnty/problems/validation.py +127 -0
  25. qnty/quantities/__init__.py +28 -5
  26. qnty/quantities/base_qnty.py +677 -0
  27. qnty/quantities/field_converters.py +24004 -0
  28. qnty/quantities/field_qnty.py +1012 -0
  29. qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
  30. qnty/{generated/quantities.py → quantities/field_vars.py} +829 -444
  31. qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
  32. qnty/solving/manager.py +50 -44
  33. qnty/solving/order.py +181 -133
  34. qnty/solving/solvers/__init__.py +2 -9
  35. qnty/solving/solvers/base.py +27 -37
  36. qnty/solving/solvers/iterative.py +115 -135
  37. qnty/solving/solvers/simultaneous.py +93 -165
  38. qnty/units/__init__.py +1 -0
  39. qnty/{generated/units.py → units/field_units.py} +1700 -991
  40. qnty/units/field_units.pyi +2461 -0
  41. qnty/units/prefixes.py +58 -105
  42. qnty/units/registry.py +76 -89
  43. qnty/utils/__init__.py +16 -0
  44. qnty/utils/caching/__init__.py +23 -0
  45. qnty/utils/caching/manager.py +401 -0
  46. qnty/utils/error_handling/__init__.py +66 -0
  47. qnty/utils/error_handling/context.py +39 -0
  48. qnty/utils/error_handling/exceptions.py +96 -0
  49. qnty/utils/error_handling/handlers.py +171 -0
  50. qnty/utils/logging.py +4 -4
  51. qnty/utils/protocols.py +164 -0
  52. qnty/utils/scope_discovery.py +420 -0
  53. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/METADATA +1 -1
  54. qnty-0.1.1.dist-info/RECORD +60 -0
  55. qnty/_backup/problem_original.py +0 -1251
  56. qnty/_backup/quantity.py +0 -63
  57. qnty/codegen/cli.py +0 -125
  58. qnty/codegen/generators/data/unit_data.json +0 -8807
  59. qnty/codegen/generators/data_processor.py +0 -345
  60. qnty/codegen/generators/dimensions_gen.py +0 -434
  61. qnty/codegen/generators/doc_generator.py +0 -141
  62. qnty/codegen/generators/out/dimension_mapping.json +0 -974
  63. qnty/codegen/generators/out/dimension_metadata.json +0 -123
  64. qnty/codegen/generators/out/units_metadata.json +0 -223
  65. qnty/codegen/generators/quantities_gen.py +0 -159
  66. qnty/codegen/generators/setters_gen.py +0 -178
  67. qnty/codegen/generators/stubs_gen.py +0 -167
  68. qnty/codegen/generators/units_gen.py +0 -295
  69. qnty/expressions/cache.py +0 -94
  70. qnty/generated/dimensions.py +0 -514
  71. qnty/problem/__init__.py +0 -91
  72. qnty/problem/base.py +0 -142
  73. qnty/problem/composition.py +0 -385
  74. qnty/problem/composition_mixin.py +0 -382
  75. qnty/problem/equations.py +0 -413
  76. qnty/problem/metaclass.py +0 -302
  77. qnty/problem/reconstruction.py +0 -1016
  78. qnty/problem/solving.py +0 -180
  79. qnty/problem/validation.py +0 -64
  80. qnty/problem/variables.py +0 -239
  81. qnty/quantities/expression_quantity.py +0 -314
  82. qnty/quantities/quantity.py +0 -428
  83. qnty/quantities/typed_quantity.py +0 -215
  84. qnty/validation/__init__.py +0 -0
  85. qnty/validation/registry.py +0 -0
  86. qnty/validation/rules.py +0 -167
  87. qnty-0.0.9.dist-info/RECORD +0 -63
  88. /qnty/{codegen → extensions}/__init__.py +0 -0
  89. /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
  90. /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
  91. /qnty/{generated → extensions/reporting}/__init__.py +0 -0
  92. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,401 @@
1
+ """
2
+ Unified Cache Management System
3
+ ===============================
4
+
5
+ Centralized caching infrastructure for all qnty operations, providing
6
+ consistent cache policies, monitoring, and memory management.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, TypeVar
12
+ from weakref import WeakValueDictionary
13
+
14
+ # Type variables for cache keys and values
15
+ K = TypeVar("K")
16
+ V = TypeVar("V")
17
+
18
+
19
+ class CacheStats:
20
+ """Statistics tracking for cache performance analysis."""
21
+
22
+ def __init__(self, name: str):
23
+ self.name = name
24
+ self.hits = 0
25
+ self.misses = 0
26
+ self.evictions = 0
27
+
28
+ def hit(self):
29
+ """Record a cache hit."""
30
+ self.hits += 1
31
+
32
+ def miss(self):
33
+ """Record a cache miss."""
34
+ self.misses += 1
35
+
36
+ def evict(self):
37
+ """Record a cache eviction."""
38
+ self.evictions += 1
39
+
40
+ @property
41
+ def hit_rate(self) -> float:
42
+ """Calculate cache hit rate."""
43
+ total = self.hits + self.misses
44
+ return self.hits / total if total > 0 else 0.0
45
+
46
+ def __str__(self) -> str:
47
+ return f"{self.name}: {self.hits}/{self.hits + self.misses} hits ({self.hit_rate:.1%}), {self.evictions} evictions"
48
+
49
+
50
+ class UnifiedCacheManager:
51
+ """
52
+ Centralized cache manager for all qnty operations.
53
+
54
+ Provides consistent caching policies, memory management, and performance
55
+ monitoring across all library components.
56
+ """
57
+
58
+ # Cache size limits (tuned based on typical usage patterns)
59
+ UNIT_PROPERTY_CACHE_SIZE = 200
60
+ AVAILABLE_UNITS_CACHE_SIZE = 50
61
+ EXPRESSION_CACHE_SIZE = 300
62
+ TYPE_CHECK_CACHE_SIZE = 100
63
+ DIMENSIONLESS_CACHE_SIZE = 50
64
+ DIMENSION_SIGNATURE_CACHE_SIZE = 500
65
+ VALIDATION_CACHE_SIZE = 150
66
+ SMALL_INTEGER_CACHE_SIZE = 100
67
+ MULTIPLICATION_CACHE_SIZE = 100
68
+ DIVISION_CACHE_SIZE = 100
69
+
70
+ def __init__(self):
71
+ # Core caches with different policies
72
+ self._unit_property_cache: dict[tuple[type, str], str | None] = {}
73
+ self._available_units_cache: dict[type, list[str]] = {}
74
+ self._expression_result_cache: dict[str, Any] = {}
75
+ self._type_check_cache: dict[type, bool] = {}
76
+ self._dimensionless_cache: dict[float, Any] = {}
77
+ self._validation_cache: dict[tuple[type, str], bool] = {}
78
+
79
+ # Quantity-specific caches
80
+ self._small_integer_cache: dict[tuple[int, str], Any] = {}
81
+ self._multiplication_cache: dict[tuple[Any, Any], Any] = {}
82
+ self._division_cache: dict[tuple[Any, Any], Any] = {}
83
+ self._dimension_cache: dict[Any, Any] = {}
84
+
85
+ # Weak reference caches for object-based keys
86
+ self._dimension_signature_cache: WeakValueDictionary = WeakValueDictionary()
87
+
88
+ # Statistics tracking
89
+ self._stats = {
90
+ "unit_property": CacheStats("Unit Property"),
91
+ "available_units": CacheStats("Available Units"),
92
+ "expression_result": CacheStats("Expression Result"),
93
+ "type_check": CacheStats("Type Check"),
94
+ "dimensionless": CacheStats("Dimensionless"),
95
+ "validation": CacheStats("Validation"),
96
+ "small_integer": CacheStats("Small Integer"),
97
+ "multiplication": CacheStats("Multiplication"),
98
+ "division": CacheStats("Division"),
99
+ "dimension": CacheStats("Dimension"),
100
+ }
101
+
102
+ # Unit Property Cache Operations
103
+ def get_unit_property(self, setter_class: type, unit: str) -> str | None:
104
+ """Get cached unit property mapping."""
105
+ key = (setter_class, unit)
106
+ if key in self._unit_property_cache:
107
+ self._stats["unit_property"].hit()
108
+ return self._unit_property_cache[key]
109
+
110
+ self._stats["unit_property"].miss()
111
+ return None
112
+
113
+ def cache_unit_property(self, setter_class: type, unit: str, property_name: str | None) -> None:
114
+ """Cache unit property mapping."""
115
+ key = (setter_class, unit)
116
+ self._unit_property_cache[key] = property_name
117
+
118
+ # Enforce cache size limit
119
+ if len(self._unit_property_cache) > self.UNIT_PROPERTY_CACHE_SIZE:
120
+ self._evict_oldest("unit_property", self._unit_property_cache, self.UNIT_PROPERTY_CACHE_SIZE // 4)
121
+
122
+ # Available Units Cache Operations
123
+ def get_available_units(self, setter_class: type) -> list[str] | None:
124
+ """Get cached available units for a setter class."""
125
+ if setter_class in self._available_units_cache:
126
+ self._stats["available_units"].hit()
127
+ return self._available_units_cache[setter_class]
128
+
129
+ self._stats["available_units"].miss()
130
+ return None
131
+
132
+ def cache_available_units(self, setter_class: type, units: list[str]) -> None:
133
+ """Cache available units for a setter class."""
134
+ self._available_units_cache[setter_class] = units
135
+
136
+ if len(self._available_units_cache) > self.AVAILABLE_UNITS_CACHE_SIZE:
137
+ self._evict_oldest("available_units", self._available_units_cache, self.AVAILABLE_UNITS_CACHE_SIZE // 4)
138
+
139
+ # Type Check Cache Operations
140
+ def get_type_check(self, obj_type: type) -> bool | None:
141
+ """Get cached type check result."""
142
+ if obj_type in self._type_check_cache:
143
+ self._stats["type_check"].hit()
144
+ return self._type_check_cache[obj_type]
145
+
146
+ self._stats["type_check"].miss()
147
+ return None
148
+
149
+ def cache_type_check(self, obj_type: type, result: bool) -> None:
150
+ """Cache type check result."""
151
+ self._type_check_cache[obj_type] = result
152
+
153
+ if len(self._type_check_cache) > self.TYPE_CHECK_CACHE_SIZE:
154
+ self._evict_oldest("type_check", self._type_check_cache, self.TYPE_CHECK_CACHE_SIZE // 4)
155
+
156
+ # Dimensionless Quantity Cache Operations
157
+ def get_dimensionless_quantity(self, value: float) -> Any | None:
158
+ """Get cached dimensionless quantity."""
159
+ if value in self._dimensionless_cache:
160
+ self._stats["dimensionless"].hit()
161
+ return self._dimensionless_cache[value]
162
+
163
+ self._stats["dimensionless"].miss()
164
+ return None
165
+
166
+ def cache_dimensionless_quantity(self, value: float, quantity: Any) -> None:
167
+ """Cache dimensionless quantity for common values."""
168
+ # Only cache small integer/simple values to prevent memory bloat
169
+ if -10 <= value <= 10 and (value == int(value) or value in [0.5, 0.25, 0.75]):
170
+ self._dimensionless_cache[value] = quantity
171
+
172
+ if len(self._dimensionless_cache) > self.DIMENSIONLESS_CACHE_SIZE:
173
+ self._evict_oldest("dimensionless", self._dimensionless_cache, self.DIMENSIONLESS_CACHE_SIZE // 4)
174
+
175
+ # Validation Cache Operations
176
+ def get_validation_result(self, setter_class: type, unit: str) -> bool | None:
177
+ """Get cached validation result."""
178
+ key = (setter_class, unit)
179
+ if key in self._validation_cache:
180
+ self._stats["validation"].hit()
181
+ return self._validation_cache[key]
182
+
183
+ self._stats["validation"].miss()
184
+ return None
185
+
186
+ def cache_validation_result(self, setter_class: type, unit: str, is_valid: bool) -> None:
187
+ """Cache validation result."""
188
+ key = (setter_class, unit)
189
+ self._validation_cache[key] = is_valid
190
+
191
+ if len(self._validation_cache) > self.VALIDATION_CACHE_SIZE:
192
+ self._evict_oldest("validation", self._validation_cache, self.VALIDATION_CACHE_SIZE // 4)
193
+
194
+ # Expression Result Cache Operations (for future use)
195
+ def get_expression_result(self, expression_key: str) -> Any | None:
196
+ """Get cached expression evaluation result."""
197
+ if expression_key in self._expression_result_cache:
198
+ self._stats["expression_result"].hit()
199
+ return self._expression_result_cache[expression_key]
200
+
201
+ self._stats["expression_result"].miss()
202
+ return None
203
+
204
+ def cache_expression_result(self, expression_key: str, result: Any) -> None:
205
+ """Cache expression evaluation result."""
206
+ self._expression_result_cache[expression_key] = result
207
+
208
+ if len(self._expression_result_cache) > self.EXPRESSION_CACHE_SIZE:
209
+ self._evict_oldest("expression_result", self._expression_result_cache, self.EXPRESSION_CACHE_SIZE // 4)
210
+
211
+ # Quantity-specific Cache Operations
212
+ def get_cached_quantity(self, value: int | float, unit_name: str) -> Any | None:
213
+ """Get cached quantity for small integers."""
214
+ if isinstance(value, int | float) and -10 <= value <= 10 and value == int(value):
215
+ key = (int(value), unit_name)
216
+ if key in self._small_integer_cache:
217
+ self._stats["small_integer"].hit()
218
+ return self._small_integer_cache[key]
219
+
220
+ self._stats["small_integer"].miss()
221
+ return None
222
+
223
+ def cache_quantity(self, value: int | float, unit_name: str, quantity: Any) -> None:
224
+ """Cache quantity if it's a small integer."""
225
+ if isinstance(value, int | float) and -10 <= value <= 10 and value == int(value):
226
+ key = (int(value), unit_name)
227
+ self._small_integer_cache[key] = quantity
228
+
229
+ if len(self._small_integer_cache) > self.SMALL_INTEGER_CACHE_SIZE:
230
+ self._evict_oldest("small_integer", self._small_integer_cache, self.SMALL_INTEGER_CACHE_SIZE // 4)
231
+
232
+ def get_multiplication_result(self, left_sig: Any, right_sig: Any) -> Any | None:
233
+ """Get cached multiplication result."""
234
+ key = (left_sig, right_sig)
235
+ if key in self._multiplication_cache:
236
+ self._stats["multiplication"].hit()
237
+ return self._multiplication_cache[key]
238
+
239
+ self._stats["multiplication"].miss()
240
+ return None
241
+
242
+ def cache_multiplication_result(self, left_sig: Any, right_sig: Any, result_unit: Any) -> None:
243
+ """Cache multiplication result."""
244
+ key = (left_sig, right_sig)
245
+ self._multiplication_cache[key] = result_unit
246
+
247
+ if len(self._multiplication_cache) > self.MULTIPLICATION_CACHE_SIZE:
248
+ self._evict_oldest("multiplication", self._multiplication_cache, self.MULTIPLICATION_CACHE_SIZE // 4)
249
+
250
+ def get_division_result(self, left_sig: Any, right_sig: Any) -> Any | None:
251
+ """Get cached division result."""
252
+ key = (left_sig, right_sig)
253
+ if key in self._division_cache:
254
+ self._stats["division"].hit()
255
+ return self._division_cache[key]
256
+
257
+ self._stats["division"].miss()
258
+ return None
259
+
260
+ def cache_division_result(self, left_sig: Any, right_sig: Any, result_unit: Any) -> None:
261
+ """Cache division result."""
262
+ key = (left_sig, right_sig)
263
+ self._division_cache[key] = result_unit
264
+
265
+ if len(self._division_cache) > self.DIVISION_CACHE_SIZE:
266
+ self._evict_oldest("division", self._division_cache, self.DIVISION_CACHE_SIZE // 4)
267
+
268
+ def get_dimension_unit(self, dimension_sig: Any) -> Any | None:
269
+ """Get cached unit for dimension signature."""
270
+ if dimension_sig in self._dimension_cache:
271
+ self._stats["dimension"].hit()
272
+ return self._dimension_cache[dimension_sig]
273
+
274
+ self._stats["dimension"].miss()
275
+ return None
276
+
277
+ def cache_dimension_unit(self, dimension_sig: Any, unit: Any) -> None:
278
+ """Cache unit for dimension signature."""
279
+ self._dimension_cache[dimension_sig] = unit
280
+
281
+ def initialize_dimension_cache(self, initial_mappings: dict[Any, Any]) -> None:
282
+ """Initialize dimension cache with common mappings."""
283
+ self._dimension_cache.update(initial_mappings)
284
+
285
+ def initialize_operation_caches(self, multiplication_mappings: dict[tuple[Any, Any], Any], division_mappings: dict[tuple[Any, Any], Any]) -> None:
286
+ """Initialize multiplication and division caches with common operations."""
287
+ self._multiplication_cache.update(multiplication_mappings)
288
+ self._division_cache.update(division_mappings)
289
+
290
+ # Cache Management Operations
291
+ def _evict_oldest(self, cache_name: str, cache_dict: dict, evict_count: int) -> None:
292
+ """Evict oldest entries from cache."""
293
+ # Simple FIFO eviction - remove first N items
294
+ keys_to_remove = list(cache_dict.keys())[:evict_count]
295
+ for key in keys_to_remove:
296
+ del cache_dict[key]
297
+ self._stats[cache_name].evict()
298
+
299
+ def clear_cache(self, cache_name: str | None = None) -> None:
300
+ """Clear specific cache or all caches."""
301
+ if cache_name is None:
302
+ # Clear all caches
303
+ self._unit_property_cache.clear()
304
+ self._available_units_cache.clear()
305
+ self._expression_result_cache.clear()
306
+ self._type_check_cache.clear()
307
+ self._dimensionless_cache.clear()
308
+ self._validation_cache.clear()
309
+ self._dimension_signature_cache.clear()
310
+ self._small_integer_cache.clear()
311
+ self._multiplication_cache.clear()
312
+ self._division_cache.clear()
313
+ self._dimension_cache.clear()
314
+ else:
315
+ # Clear specific cache
316
+ cache_map = {
317
+ "unit_property": self._unit_property_cache,
318
+ "available_units": self._available_units_cache,
319
+ "expression_result": self._expression_result_cache,
320
+ "type_check": self._type_check_cache,
321
+ "dimensionless": self._dimensionless_cache,
322
+ "validation": self._validation_cache,
323
+ "small_integer": self._small_integer_cache,
324
+ "multiplication": self._multiplication_cache,
325
+ "division": self._division_cache,
326
+ "dimension": self._dimension_cache,
327
+ }
328
+ if cache_name in cache_map:
329
+ cache_map[cache_name].clear()
330
+
331
+ def get_cache_stats(self) -> dict[str, CacheStats]:
332
+ """Get performance statistics for all caches."""
333
+ return self._stats.copy()
334
+
335
+ def get_cache_sizes(self) -> dict[str, int]:
336
+ """Get current size of all caches."""
337
+ return {
338
+ "unit_property": len(self._unit_property_cache),
339
+ "available_units": len(self._available_units_cache),
340
+ "expression_result": len(self._expression_result_cache),
341
+ "type_check": len(self._type_check_cache),
342
+ "dimensionless": len(self._dimensionless_cache),
343
+ "validation": len(self._validation_cache),
344
+ "dimension_signature": len(self._dimension_signature_cache),
345
+ "small_integer": len(self._small_integer_cache),
346
+ "multiplication": len(self._multiplication_cache),
347
+ "division": len(self._division_cache),
348
+ "dimension": len(self._dimension_cache),
349
+ }
350
+
351
+ def get_memory_usage_estimate(self) -> int:
352
+ """Estimate memory usage in bytes (rough approximation)."""
353
+ # Rough estimates based on typical key/value sizes
354
+ estimates = {
355
+ "unit_property": len(self._unit_property_cache) * 200, # tuple keys + string values
356
+ "available_units": len(self._available_units_cache) * 500, # type keys + list values
357
+ "expression_result": len(self._expression_result_cache) * 300, # string keys + object values
358
+ "type_check": len(self._type_check_cache) * 100, # type keys + bool values
359
+ "dimensionless": len(self._dimensionless_cache) * 150, # float keys + quantity values
360
+ "validation": len(self._validation_cache) * 150, # tuple keys + bool values
361
+ "small_integer": len(self._small_integer_cache) * 200, # tuple keys + quantity values
362
+ "multiplication": len(self._multiplication_cache) * 250, # tuple keys + unit values
363
+ "division": len(self._division_cache) * 250, # tuple keys + unit values
364
+ "dimension": len(self._dimension_cache) * 200, # signature keys + unit values
365
+ }
366
+ return sum(estimates.values())
367
+
368
+
369
+ # Global cache manager instance
370
+ _cache_manager: UnifiedCacheManager | None = None
371
+
372
+
373
+ def get_cache_manager() -> UnifiedCacheManager:
374
+ """Get the global cache manager instance."""
375
+ global _cache_manager
376
+ if _cache_manager is None:
377
+ _cache_manager = UnifiedCacheManager()
378
+ return _cache_manager
379
+
380
+
381
+ # Convenience functions for common operations
382
+ def clear_all_caches() -> None:
383
+ """Clear all caches - useful for testing and memory management."""
384
+ get_cache_manager().clear_cache()
385
+
386
+
387
+ def get_cache_statistics() -> dict[str, str]:
388
+ """Get formatted cache statistics."""
389
+ stats = get_cache_manager().get_cache_stats()
390
+ return {name: str(stat) for name, stat in stats.items()}
391
+
392
+
393
+ def get_memory_usage() -> str:
394
+ """Get formatted memory usage estimate."""
395
+ bytes_used = get_cache_manager().get_memory_usage_estimate()
396
+ if bytes_used < 1024:
397
+ return f"{bytes_used} bytes"
398
+ elif bytes_used < 1024 * 1024:
399
+ return f"{bytes_used / 1024:.1f} KB"
400
+ else:
401
+ return f"{bytes_used / (1024 * 1024):.1f} MB"
@@ -0,0 +1,66 @@
1
+ """
2
+ Error handling infrastructure for the Qnty library.
3
+
4
+ This package provides comprehensive error handling capabilities including
5
+ custom exceptions, error handlers, and context management.
6
+
7
+ Public API:
8
+ - Exception classes: QntyError, DimensionalError, UnitConversionError, etc.
9
+ - Error handling: ErrorHandler, ErrorHandlerMixin
10
+ - Context management: ErrorContext
11
+ - Convenience functions: require_variable, ensure_not_zero, safe_evaluate
12
+ """
13
+
14
+ # Import all exception classes
15
+ # Import context management
16
+ from .context import (
17
+ ErrorContext,
18
+ create_context,
19
+ get_dimension_string,
20
+ )
21
+ from .exceptions import (
22
+ ERROR_MESSAGES,
23
+ DimensionalError,
24
+ DivisionByZeroError,
25
+ EquationSolvingError,
26
+ ExpressionEvaluationError,
27
+ QntyError,
28
+ UnitConversionError,
29
+ VariableNotFoundError,
30
+ )
31
+
32
+ # Import handlers and utilities
33
+ from .handlers import (
34
+ ErrorHandler,
35
+ ErrorHandlerMixin,
36
+ ensure_not_zero,
37
+ get_error_handler,
38
+ require_variable,
39
+ safe_evaluate,
40
+ set_error_handler,
41
+ )
42
+
43
+ # Define public API
44
+ __all__ = [
45
+ # Exception classes
46
+ "QntyError",
47
+ "DimensionalError",
48
+ "UnitConversionError",
49
+ "VariableNotFoundError",
50
+ "EquationSolvingError",
51
+ "ExpressionEvaluationError",
52
+ "DivisionByZeroError",
53
+ "ERROR_MESSAGES",
54
+ # Context management
55
+ "ErrorContext",
56
+ "get_dimension_string",
57
+ "create_context",
58
+ # Handlers and utilities
59
+ "ErrorHandler",
60
+ "ErrorHandlerMixin",
61
+ "get_error_handler",
62
+ "set_error_handler",
63
+ "require_variable",
64
+ "ensure_not_zero",
65
+ "safe_evaluate",
66
+ ]
@@ -0,0 +1,39 @@
1
+ """
2
+ Error context and utility functions for error handling.
3
+
4
+ This module provides context management and utility functions to support
5
+ consistent error reporting throughout the library.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+
12
+ @dataclass
13
+ class ErrorContext:
14
+ """Context information for errors to aid in debugging."""
15
+
16
+ module: str
17
+ function: str
18
+ operation: str
19
+ variables: dict[str, Any] | None = None
20
+ additional_info: dict[str, Any] | None = None
21
+
22
+ def to_dict(self) -> dict[str, Any]:
23
+ """Convert to dictionary for error logging."""
24
+ return {"module": self.module, "function": self.function, "operation": self.operation, "variables": self.variables, "additional_info": self.additional_info}
25
+
26
+
27
+ def get_dimension_string(obj: Any) -> str:
28
+ """Get dimension string representation for error messages."""
29
+ if hasattr(obj, "_dimension_sig"):
30
+ return str(obj._dimension_sig)
31
+ elif hasattr(obj, "unit") and hasattr(obj.unit, "dimension"):
32
+ return str(obj.unit.dimension)
33
+ else:
34
+ return f"{type(obj).__name__}"
35
+
36
+
37
+ def create_context(module: str, function: str, operation: str, **kwargs) -> ErrorContext:
38
+ """Create error context for consistent error reporting."""
39
+ return ErrorContext(module=module, function=function, operation=operation, additional_info=kwargs)
@@ -0,0 +1,96 @@
1
+ """
2
+ Exception classes for the Qnty library.
3
+
4
+ This module defines all custom exception types used throughout the library
5
+ for consistent error handling and reporting.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+
11
+ # Custom Exception Hierarchy
12
+ class QntyError(Exception):
13
+ """Base exception for all qnty library errors."""
14
+
15
+ def __init__(self, message: str, context: dict[str, Any] | None = None):
16
+ super().__init__(message)
17
+ self.context = context or {}
18
+ self.message = message
19
+
20
+
21
+ class DimensionalError(QntyError):
22
+ """Raised when operations involve incompatible dimensions."""
23
+
24
+ def __init__(self, operation: str, left_dim: str, right_dim: str, context: dict | None = None):
25
+ message = f"Incompatible dimensions for {operation}: {left_dim} and {right_dim}"
26
+ super().__init__(message, context)
27
+ self.operation = operation
28
+ self.left_dimension = left_dim
29
+ self.right_dimension = right_dim
30
+
31
+
32
+ class UnitConversionError(QntyError):
33
+ """Raised when unit conversions fail."""
34
+
35
+ def __init__(self, from_unit: str, to_unit: str, reason: str = "", context: dict | None = None):
36
+ message = f"Cannot convert from '{from_unit}' to '{to_unit}'"
37
+ if reason:
38
+ message += f": {reason}"
39
+ super().__init__(message, context)
40
+ self.from_unit = from_unit
41
+ self.to_unit = to_unit
42
+
43
+
44
+ class VariableNotFoundError(QntyError):
45
+ """Raised when a required variable is not found."""
46
+
47
+ def __init__(self, variable_name: str, available_vars: list[str] | None = None, context: dict | None = None):
48
+ message = f"Variable '{variable_name}' not found"
49
+ if available_vars:
50
+ message += f". Available variables: {', '.join(available_vars)}"
51
+ super().__init__(message, context)
52
+ self.variable_name = variable_name
53
+ self.available_vars = available_vars or []
54
+
55
+
56
+ class EquationSolvingError(QntyError):
57
+ """Raised when equation solving fails."""
58
+
59
+ def __init__(self, equation_name: str, target_var: str, reason: str = "", context: dict | None = None):
60
+ message = f"Cannot solve equation '{equation_name}' for variable '{target_var}'"
61
+ if reason:
62
+ message += f": {reason}"
63
+ super().__init__(message, context)
64
+ self.equation_name = equation_name
65
+ self.target_var = target_var
66
+
67
+
68
+ class ExpressionEvaluationError(QntyError):
69
+ """Raised when expression evaluation fails."""
70
+
71
+ def __init__(self, expression: str, reason: str = "", context: dict | None = None):
72
+ message = f"Cannot evaluate expression '{expression}'"
73
+ if reason:
74
+ message += f": {reason}"
75
+ super().__init__(message, context)
76
+ self.expression = expression
77
+
78
+
79
+ class DivisionByZeroError(QntyError):
80
+ """Raised for division by zero operations."""
81
+
82
+ def __init__(self, dividend: str, context: dict | None = None):
83
+ message = f"Division by zero: {dividend} / 0"
84
+ super().__init__(message, context)
85
+ self.dividend = dividend
86
+
87
+
88
+ # Error message templates for consistency
89
+ ERROR_MESSAGES = {
90
+ "incompatible_dimensions": "Incompatible dimensions: {left} and {right} for operation {operation}",
91
+ "variable_not_found": "Variable '{variable}' not found. Available: {available}",
92
+ "unit_conversion_failed": "Cannot convert from '{from_unit}' to '{to_unit}': {reason}",
93
+ "equation_unsolvable": "Cannot solve equation '{equation}' for variable '{variable}': {reason}",
94
+ "division_by_zero": "Division by zero in expression: {expression}",
95
+ "invalid_operation": "Invalid operation '{operation}' on {type}: {reason}",
96
+ }