Skip to content

Commit 8521ee4

Browse files
committed
removed implicit singleton behavior of Tolerance. Independent instances can be created and global instance can be explicitly modified
1 parent e34268a commit 8521ee4

3 files changed

Lines changed: 293 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added `TOL.update()` method for explicit global state modification.
13+
* Added `TOL.temporary()` context manager for scoped changes.
14+
1215
### Changed
1316

17+
* Changed `Tolerance` class to no longer use singleton pattern. `Tolerance()` now creates independent instances instead of returning the global `TOL`.
18+
1419
### Removed
1520

1621

src/compas/tolerance.py

Lines changed: 234 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
11
"""
22
The tolerance module provides functionality to deal with tolerances consistently across all other COMPAS packages.
3+
4+
The module provides:
5+
- :class:`Tolerance`: A class for tolerance settings that can be instantiated independently.
6+
- :obj:`TOL`: The global tolerance instance used throughout COMPAS (in-process).
7+
8+
To modify global tolerance settings, use the explicit methods on `TOL`:
9+
- ``TOL.update(...)`` - Update specific tolerance values
10+
- ``TOL.reset()`` - Reset to default values
11+
- ``TOL.temporary(...)`` - Context manager for temporary changes
12+
13+
Example
14+
-------
15+
>>> from compas.tolerance import TOL, Tolerance
16+
>>> # Create an independent tolerance instance
17+
>>> my_tol = Tolerance(absolute=0.01)
18+
>>> my_tol.absolute
19+
0.01
20+
>>> # Global TOL is unchanged
21+
>>> TOL.absolute
22+
1e-09
23+
>>> # To modify global state, use update()
24+
>>> TOL.update(absolute=0.001)
25+
>>> TOL.absolute
26+
0.001
27+
>>> TOL.reset()
28+
329
"""
430

531
from __future__ import absolute_import
632
from __future__ import division
733
from __future__ import print_function
834

35+
from contextlib import contextmanager
936
from decimal import Decimal
1037

1138
import compas
@@ -21,6 +48,21 @@ class Tolerance(Data):
2148
----------
2249
unit : {"M", "MM"}, optional
2350
The unit of the tolerance settings.
51+
absolute : float, optional
52+
The absolute tolerance. Default is :attr:`ABSOLUTE`.
53+
relative : float, optional
54+
The relative tolerance. Default is :attr:`RELATIVE`.
55+
angular : float, optional
56+
The angular tolerance. Default is :attr:`ANGULAR`.
57+
approximation : float, optional
58+
The tolerance used in approximation processes. Default is :attr:`APPROXIMATION`.
59+
precision : int, optional
60+
The precision used when converting numbers to strings. Default is :attr:`PRECISION`.
61+
lineardeflection : float, optional
62+
The maximum distance between a curve/surface and its polygonal approximation.
63+
Default is :attr:`LINEARDEFLECTION`.
64+
angulardeflection : float, optional
65+
The maximum curvature deviation. Default is :attr:`ANGULARDEFLECTION`.
2466
name : str, optional
2567
The name of the tolerance settings.
2668
@@ -53,25 +95,35 @@ class Tolerance(Data):
5395
This value is called the "true value".
5496
By convention, the second value is considered the "true value" by the comparison functions of this class.
5597
56-
The :class:`compas.tolerance.Tolerance` class is implemented using a "singleton" pattern and can therefore have only 1 (one) instance per context.
57-
Usage of :attr:`compas.tolerance.TOL` outside of :mod:`compas` internals is therefore deprecated.
98+
Each call to ``Tolerance(...)`` creates an independent instance. To modify the global
99+
tolerance settings used throughout COMPAS, use the explicit methods on :obj:`TOL`:
100+
101+
- ``TOL.update(...)`` - Update specific tolerance values
102+
- ``TOL.reset()`` - Reset all values to defaults
103+
- ``TOL.temporary(...)`` - Context manager for temporary changes
58104
59105
Examples
60106
--------
61-
>>> tol = Tolerance()
62-
>>> tol.unit
63-
'M'
107+
Create an independent tolerance instance:
108+
109+
>>> tol = Tolerance(absolute=0.01)
64110
>>> tol.absolute
111+
0.01
112+
113+
The global TOL is separate:
114+
115+
>>> from compas.tolerance import TOL
116+
>>> TOL.absolute # unchanged
65117
1e-09
66-
>>> tol.relative
67-
1e-06
68-
>>> tol.angular
69-
1e-06
70118
71-
"""
119+
Modify global state explicitly:
72120
73-
_instance = None
74-
_is_inited = False
121+
>>> TOL.update(absolute=0.001)
122+
>>> TOL.absolute
123+
0.001
124+
>>> TOL.reset()
125+
126+
"""
75127

76128
SUPPORTED_UNITS = ["M", "MM"]
77129
"""{"M", "MM"}: Default tolerances are defined in relation to length units.
@@ -120,12 +172,6 @@ class Tolerance(Data):
120172
121173
"""
122174

123-
def __new__(cls, *args, **kwargs):
124-
if not cls._instance:
125-
cls._instance = object.__new__(cls, *args, **kwargs)
126-
cls._is_inited = False
127-
return cls._instance
128-
129175
@property
130176
def __data__(self):
131177
return {
@@ -160,22 +206,19 @@ def __init__(
160206
angular=None,
161207
approximation=None,
162208
precision=None,
163-
lineardflection=None,
164-
angulardflection=None,
209+
lineardeflection=None,
210+
angulardeflection=None,
165211
name=None,
166212
):
167213
super(Tolerance, self).__init__(name=name)
168-
if not self._is_inited:
169-
self._unit = None
170-
self._absolute = None
171-
self._relative = None
172-
self._angular = None
173-
self._approximation = None
174-
self._precision = None
175-
self._lineardeflection = None
176-
self._angulardeflection = None
177-
178-
self._is_inited = True
214+
self._unit = None
215+
self._absolute = None
216+
self._relative = None
217+
self._angular = None
218+
self._approximation = None
219+
self._precision = None
220+
self._lineardeflection = None
221+
self._angulardeflection = None
179222

180223
if unit is not None:
181224
self.unit = unit
@@ -189,13 +232,10 @@ def __init__(
189232
self.approximation = approximation
190233
if precision is not None:
191234
self.precision = precision
192-
if lineardflection is not None:
193-
self.lineardeflection = lineardflection
194-
if angulardflection is not None:
195-
self.angulardeflection = angulardflection
196-
197-
# this can be autogenerated if we use slots
198-
# __repr__: return f"{__class__.__name__}({', '.join(f'{k}={v!r}' for k, v in self.__dict__.items())})}"
235+
if lineardeflection is not None:
236+
self.lineardeflection = lineardeflection
237+
if angulardeflection is not None:
238+
self.angulardeflection = angulardeflection
199239

200240
def __repr__(self):
201241
return "Tolerance(unit='{}', absolute={}, relative={}, angular={}, approximation={}, precision={}, lineardeflection={}, angulardeflection={})".format(
@@ -220,7 +260,7 @@ def reset(self):
220260
self._angulardeflection = None
221261

222262
def update_from_dict(self, tolerance):
223-
"""Update the tolerance singleton from the key-value pairs found in a dict.
263+
"""Update the tolerance from the key-value pairs found in a dict.
224264
225265
Parameters
226266
----------
@@ -236,6 +276,161 @@ def update_from_dict(self, tolerance):
236276
if hasattr(self, name):
237277
setattr(self, name, tolerance[name])
238278

279+
def update(
280+
self,
281+
unit=None,
282+
absolute=None,
283+
relative=None,
284+
angular=None,
285+
approximation=None,
286+
precision=None,
287+
lineardeflection=None,
288+
angulardeflection=None,
289+
):
290+
"""Update tolerance settings.
291+
292+
Only the provided parameters will be updated; others remain unchanged.
293+
Use this method to explicitly modify tolerance settings.
294+
295+
Parameters
296+
----------
297+
unit : {"M", "MM"}, optional
298+
The unit of the tolerance settings.
299+
absolute : float, optional
300+
The absolute tolerance.
301+
relative : float, optional
302+
The relative tolerance.
303+
angular : float, optional
304+
The angular tolerance.
305+
approximation : float, optional
306+
The tolerance used in approximation processes.
307+
precision : int, optional
308+
The precision used when converting numbers to strings.
309+
lineardeflection : float, optional
310+
The maximum distance between a curve/surface and its polygonal approximation.
311+
angulardeflection : float, optional
312+
The maximum curvature deviation.
313+
314+
Returns
315+
-------
316+
None
317+
318+
Examples
319+
--------
320+
>>> from compas.tolerance import TOL
321+
>>> TOL.update(absolute=0.001, precision=6)
322+
>>> TOL.absolute
323+
0.001
324+
>>> TOL.precision
325+
6
326+
>>> TOL.reset()
327+
328+
"""
329+
if unit is not None:
330+
self.unit = unit
331+
if absolute is not None:
332+
self.absolute = absolute
333+
if relative is not None:
334+
self.relative = relative
335+
if angular is not None:
336+
self.angular = angular
337+
if approximation is not None:
338+
self.approximation = approximation
339+
if precision is not None:
340+
self.precision = precision
341+
if lineardeflection is not None:
342+
self.lineardeflection = lineardeflection
343+
if angulardeflection is not None:
344+
self.angulardeflection = angulardeflection
345+
346+
@contextmanager
347+
def temporary(
348+
self,
349+
unit=None,
350+
absolute=None,
351+
relative=None,
352+
angular=None,
353+
approximation=None,
354+
precision=None,
355+
lineardeflection=None,
356+
angulardeflection=None,
357+
):
358+
"""Context manager for temporarily changing tolerance settings.
359+
360+
The original settings are automatically restored when the context exits,
361+
even if an exception occurs.
362+
363+
Parameters
364+
----------
365+
unit : {"M", "MM"}, optional
366+
The unit of the tolerance settings.
367+
absolute : float, optional
368+
The absolute tolerance.
369+
relative : float, optional
370+
The relative tolerance.
371+
angular : float, optional
372+
The angular tolerance.
373+
approximation : float, optional
374+
The tolerance used in approximation processes.
375+
precision : int, optional
376+
The precision used when converting numbers to strings.
377+
lineardeflection : float, optional
378+
The maximum distance between a curve/surface and its polygonal approximation.
379+
angulardeflection : float, optional
380+
The maximum curvature deviation.
381+
382+
Yields
383+
------
384+
:class:`Tolerance`
385+
The tolerance instance with temporary settings applied.
386+
387+
Examples
388+
--------
389+
>>> from compas.tolerance import TOL
390+
>>> TOL.absolute
391+
1e-09
392+
>>> with TOL.temporary(absolute=0.01):
393+
... TOL.absolute
394+
0.01
395+
>>> TOL.absolute
396+
1e-09
397+
398+
"""
399+
# Save current state
400+
saved = {
401+
"unit": self._unit,
402+
"absolute": self._absolute,
403+
"relative": self._relative,
404+
"angular": self._angular,
405+
"approximation": self._approximation,
406+
"precision": self._precision,
407+
"lineardeflection": self._lineardeflection,
408+
"angulardeflection": self._angulardeflection,
409+
}
410+
try:
411+
# Apply temporary changes
412+
self.update(
413+
unit=unit,
414+
absolute=absolute,
415+
relative=relative,
416+
angular=angular,
417+
approximation=approximation,
418+
precision=precision,
419+
lineardeflection=lineardeflection,
420+
angulardeflection=angulardeflection,
421+
)
422+
yield self
423+
finally:
424+
# Restore original state
425+
self._unit = saved["unit"]
426+
self._absolute = saved["absolute"]
427+
self._relative = saved["relative"]
428+
self._angular = saved["angular"]
429+
self._approximation = saved["approximation"]
430+
self._precision = saved["precision"]
431+
self._lineardeflection = saved["lineardeflection"]
432+
self._angulardeflection = saved["angulardeflection"]
433+
239434
@property
240435
def units(self):
241436
return self._unit

0 commit comments

Comments
 (0)