Skip to content

Commit 8fa70b8

Browse files
authored
Merge pull request #1510 from compas-dev/tolerance
Removed implicit singleton behavior of Tolerance.
2 parents e34268a + 26c9132 commit 8fa70b8

File tree

3 files changed

+320
-42
lines changed

3 files changed

+320
-42
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ 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+
* Renamed `Tolerance.units` to `Tolerance.unit` to better reflect the documented properties. Left `units` with deprecation warning.
19+
1420
### Removed
1521

1622

src/compas/tolerance.py

Lines changed: 250 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
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
37+
from warnings import warn
1038

1139
import compas
1240
from compas.data import Data
@@ -21,6 +49,21 @@ class Tolerance(Data):
2149
----------
2250
unit : {"M", "MM"}, optional
2351
The unit of the tolerance settings.
52+
absolute : float, optional
53+
The absolute tolerance. Default is :attr:`ABSOLUTE`.
54+
relative : float, optional
55+
The relative tolerance. Default is :attr:`RELATIVE`.
56+
angular : float, optional
57+
The angular tolerance. Default is :attr:`ANGULAR`.
58+
approximation : float, optional
59+
The tolerance used in approximation processes. Default is :attr:`APPROXIMATION`.
60+
precision : int, optional
61+
The precision used when converting numbers to strings. Default is :attr:`PRECISION`.
62+
lineardeflection : float, optional
63+
The maximum distance between a curve/surface and its polygonal approximation.
64+
Default is :attr:`LINEARDEFLECTION`.
65+
angulardeflection : float, optional
66+
The maximum curvature deviation. Default is :attr:`ANGULARDEFLECTION`.
2467
name : str, optional
2568
The name of the tolerance settings.
2669
@@ -53,25 +96,35 @@ class Tolerance(Data):
5396
This value is called the "true value".
5497
By convention, the second value is considered the "true value" by the comparison functions of this class.
5598
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.
99+
Each call to ``Tolerance(...)`` creates an independent instance. To modify the global
100+
tolerance settings used throughout COMPAS, use the explicit methods on :obj:`TOL`:
101+
102+
- ``TOL.update(...)`` - Update specific tolerance values
103+
- ``TOL.reset()`` - Reset all values to defaults
104+
- ``TOL.temporary(...)`` - Context manager for temporary changes
58105
59106
Examples
60107
--------
61-
>>> tol = Tolerance()
62-
>>> tol.unit
63-
'M'
108+
Create an independent tolerance instance:
109+
110+
>>> tol = Tolerance(absolute=0.01)
64111
>>> tol.absolute
112+
0.01
113+
114+
The global TOL is separate:
115+
116+
>>> from compas.tolerance import TOL
117+
>>> TOL.absolute # unchanged
65118
1e-09
66-
>>> tol.relative
67-
1e-06
68-
>>> tol.angular
69-
1e-06
70119
71-
"""
120+
Modify global state explicitly:
72121
73-
_instance = None
74-
_is_inited = False
122+
>>> TOL.update(absolute=0.001)
123+
>>> TOL.absolute
124+
0.001
125+
>>> TOL.reset()
126+
127+
"""
75128

76129
SUPPORTED_UNITS = ["M", "MM"]
77130
"""{"M", "MM"}: Default tolerances are defined in relation to length units.
@@ -120,12 +173,6 @@ class Tolerance(Data):
120173
121174
"""
122175

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-
129176
@property
130177
def __data__(self):
131178
return {
@@ -160,22 +207,19 @@ def __init__(
160207
angular=None,
161208
approximation=None,
162209
precision=None,
163-
lineardflection=None,
164-
angulardflection=None,
210+
lineardeflection=None,
211+
angulardeflection=None,
165212
name=None,
166213
):
167214
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
215+
self._unit = None
216+
self._absolute = None
217+
self._relative = None
218+
self._angular = None
219+
self._approximation = None
220+
self._precision = None
221+
self._lineardeflection = None
222+
self._angulardeflection = None
179223

180224
if unit is not None:
181225
self.unit = unit
@@ -189,13 +233,10 @@ def __init__(
189233
self.approximation = approximation
190234
if precision is not None:
191235
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())})}"
236+
if lineardeflection is not None:
237+
self.lineardeflection = lineardeflection
238+
if angulardeflection is not None:
239+
self.angulardeflection = angulardeflection
199240

200241
def __repr__(self):
201242
return "Tolerance(unit='{}', absolute={}, relative={}, angular={}, approximation={}, precision={}, lineardeflection={}, angulardeflection={})".format(
@@ -220,7 +261,7 @@ def reset(self):
220261
self._angulardeflection = None
221262

222263
def update_from_dict(self, tolerance):
223-
"""Update the tolerance singleton from the key-value pairs found in a dict.
264+
"""Update the tolerance from the key-value pairs found in a dict.
224265
225266
Parameters
226267
----------
@@ -236,16 +277,183 @@ def update_from_dict(self, tolerance):
236277
if hasattr(self, name):
237278
setattr(self, name, tolerance[name])
238279

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

243-
@units.setter
244-
def units(self, value):
441+
@unit.setter
442+
def unit(self, value):
245443
if value not in ["M", "MM"]:
246444
raise ValueError("Invalid unit: {}".format(value))
247445
self._unit = value
248446

447+
@property
448+
def units(self):
449+
warn("The 'units' property is deprecated. Use 'unit' instead.", DeprecationWarning)
450+
return self.unit
451+
452+
@units.setter
453+
def units(self, value):
454+
warn("The 'units' property is deprecated. Use 'unit' instead.", DeprecationWarning)
455+
self.unit = value
456+
249457
@property
250458
def absolute(self):
251459
if not self._absolute:

0 commit comments

Comments
 (0)