Skip to content

Commit cd0cf6b

Browse files
Ken KundertKen Kundert
authored andcommitted
add transparent_preferences
1 parent 8f18f18 commit cd0cf6b

File tree

7 files changed

+223
-11
lines changed

7 files changed

+223
-11
lines changed

doc/releases.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Latest development release
1212
- Add spacer keyword argument to :meth:`Quantity.render`,
1313
:meth:`Quantity.fixed` and :meth:`Quantity.binary`.
1414
- Add support for scaling with binary scale factors.
15+
- Add USD and bps unit conversions
16+
- Add support # modifier to f & g formats
17+
- Add *transparent_preferences* :class:`Quantity` attribute.
1518

1619

1720
2.20 (2024-04-27)

doc/user.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,6 +1468,41 @@ of 'K' as a scale factor to avoid confusion with Kelvin units.
14681468
5.3 kK
14691469
3.18 K
14701470
1471+
The first time a subclass is used to create a *Quantity* subclass object the
1472+
preferences from the parent class are copied into the subclass. After that
1473+
point, any changes made to the preferences of the parent class no longer affect
1474+
the subclass. However, this behavior can be altered by adding the
1475+
*transparent_preferences* attribute to the subclass definition:
1476+
1477+
.. code-block:: python
1478+
1479+
>>> class Decibels(Quantity):
1480+
... units = 'dB'
1481+
... prec=1
1482+
1483+
>>> gain = Decibels(-40)
1484+
1485+
>>> with Quantity.prefs(minus=Quantity.minus_sign):
1486+
... print(gain)
1487+
-40 dB
1488+
1489+
>>> class Decibels(Quantity):
1490+
... units = 'dB'
1491+
... prec=1
1492+
... transparent_preferences = True
1493+
1494+
>>> gain = Decibels(-40)
1495+
1496+
>>> with Quantity.prefs(minus=Quantity.minus_sign):
1497+
... print(gain)
1498+
40 dB
1499+
1500+
It is a bit subtle, but if you look carefully at the minus sign in the second
1501+
case, you can see that it looks a little longer. That is because the normal
1502+
hyphen was replaced by the unicode minus sign. That did not happen in the first
1503+
case because *Decibels* was disconnected from any changes to *Quantity*'s
1504+
preferences.
1505+
14711506

14721507
.. _scaling with subclasses:
14731508

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ requires = ["flit_core >=2,<4"]
4040
build-backend = "flit_core.buildapi"
4141

4242
[tool.ruff]
43-
exclude = [".tox", "doc", "tests", "Diffs"]
43+
exclude = [".tox", "doc", "tests", ".diffs"]
4444

4545
[tool.ruff.lint]
4646
select = ["F"]

quantiphy/quantiphy.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -658,35 +658,54 @@ class Quantity(float):
658658

659659
# preferences {{{2
660660
_initialized = False
661+
transparent_preferences = False
662+
# if set true, subclasses will use current preferences from the parent
663+
# class, even those that have changed since the subclass was created.
661664

662665
# initialize preferences {{{3
663666
@classmethod
664667
def _initialize_preferences(cls):
665-
if cls._initialized == id(cls):
666-
return
667-
cls.reset_prefs()
668+
if cls._initialized != id(cls):
669+
cls.reset_prefs()
668670

669671
# reset preferences {{{3
670672
@classmethod
671-
def reset_prefs(cls):
673+
def reset_prefs(cls, transparent=None):
672674
"""Reset preferences
673675
676+
:arg bool transparent:
677+
If true this class inherits current preferences from the parent
678+
classes. In false, the parents preferences are copied into this
679+
class the first time it is used, and any changes made to the parents
680+
preferences after first use are ignored. The default is false.
681+
674682
Resets all preferences to the current preferences of the parent class.
675683
If there is no parent class, they are reset to their defaults.
676684
"""
685+
if transparent is None:
686+
transparent = cls.transparent_preferences
687+
677688
cls._initialized = id(cls)
678689
if cls == Quantity:
690+
# this is the base class
679691
prefs = DEFAULTS
692+
transparent = False
680693
else:
694+
# this is a subclass
681695
parent = cls.__mro__[1]
682696
# for some reason I cannot get super to work right
683697
prefs = parent._preferences
684-
# copy dict so any changes made to parent's preferences do not affect us
685-
prefs = dict(prefs)
698+
699+
if not transparent:
700+
# copy dict so subsequent changes made to parent's preferences do not affect us
701+
prefs = dict(prefs)
702+
703+
cls.transparent_preferences = transparent
704+
686705
cls._preferences = ChainMap({}, prefs)
687706
# use chain to support use of contexts
688707
# put empty map in as first so user never accidentally deletes or
689-
# changes one of the initial preferences
708+
# modifies one of the initial preferences
690709

691710
# set preferences {{{3
692711
@classmethod
@@ -1139,6 +1158,7 @@ def get_pref(cls, name):
11391158
11401159
"""
11411160
cls._initialize_preferences()
1161+
11421162
try:
11431163
return getattr(cls, name, cls._preferences[name])
11441164
except KeyError:
@@ -1154,11 +1174,15 @@ def __init__(self, cls, kwargs):
11541174
def __enter__(self):
11551175
cls = self.cls
11561176
cls._initialize_preferences()
1157-
cls._preferences = cls._preferences.new_child()
1177+
cls._preferences.maps.insert(0, {})
1178+
# do not use ChainMap.new_child() as that creates a new ChainMap,
1179+
# orphaning the original, which could be being used by a subclass
11581180
cls.set_prefs(**self.kwargs)
11591181

11601182
def __exit__(self, *args):
1161-
self.cls._preferences = self.cls._preferences.parents
1183+
self.cls._preferences.maps.pop(0)
1184+
# do not use ChainMap.parents as that creates a new ChainMap,
1185+
# orphaning the original, which could be being used by a subclass
11621186

11631187
# now, return the context manager
11641188
@classmethod

tests/test_doctests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_manual():
3333
Quantity.reset_prefs()
3434
expected_test_count = {
3535
'../doc/index.rst': 31,
36-
'../doc/user.rst': 458,
36+
'../doc/user.rst': 464,
3737
'../doc/api.rst': 0,
3838
'../doc/examples.rst': 42,
3939
'../doc/accessories.rst': 12,

tests/test_format.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def test_format():
5454
assert exception.value.args[0] == "Unknown format code 'x' for object of type 'float'"
5555

5656
def test_full_format():
57+
Quantity.reset_prefs()
5758
Quantity.set_prefs(spacer=None, show_label=None, label_fmt=None, label_fmt_full=None, show_desc=False)
5859
Quantity.set_prefs(prec='full')
5960
q = Quantity('f = 1420.405751786 MHz — frequency of hydrogen line')
@@ -121,6 +122,7 @@ def test_full_format():
121122

122123

123124
def test_width():
125+
Quantity.reset_prefs()
124126
Quantity.set_prefs(spacer=None, show_label=None, label_fmt=None, label_fmt_full=None, show_desc=False)
125127
Quantity.set_prefs(prec='full')
126128
q=Quantity('f = 1420.405751786 MHz -- frequency of hydrogen line')
@@ -154,6 +156,7 @@ def test_width():
154156
assert '{:#25.0p}'.format(q) == ' 1420405752. Hz'
155157

156158
def test_currency():
159+
Quantity.reset_prefs()
157160
Quantity.set_prefs(
158161
spacer = None,
159162
show_label = None,
@@ -197,6 +200,7 @@ def test_currency():
197200
assert float(q) == 2e-9
198201

199202
def test_exceptional():
203+
Quantity.reset_prefs()
200204
Quantity.set_prefs(spacer=None, show_label=None, label_fmt=None, label_fmt_full=None, show_desc=False)
201205
Quantity.set_prefs(prec='full')
202206
q=Quantity('light = inf Hz -- a high frequency')
@@ -223,6 +227,7 @@ def test_exceptional():
223227
assert '{:,.2P}'.format(q) == 'light = inf Hz'
224228

225229
def test_scaled_format():
230+
Quantity.reset_prefs()
226231
Quantity.set_prefs(spacer=None, show_label=None, label_fmt=None, label_fmt_full=None, show_desc=False)
227232
Quantity.set_prefs(prec=None)
228233
q=Quantity('Tboil = 100 °C -- boiling point of water')
@@ -255,6 +260,7 @@ def test_scaled_format():
255260
assert '{:#,.2P°F}'.format(q) == 'Tboil = 212.00 °F'
256261

257262
def test_number_fmt():
263+
Quantity.reset_prefs()
258264
Quantity.set_prefs(spacer=None, show_label=None, label_fmt=None, label_fmt_full=None, show_desc=False)
259265
Quantity.set_prefs(prec=None)
260266
with Quantity.prefs(number_fmt='{whole:>3s}{frac:<4s} {units:<2s}'):
@@ -331,6 +337,7 @@ def fmt_num(whole, frac, units):
331337
assert '<{:s}>'.format(Quantity('123,4 mm')) == '<123,4 mm>'
332338

333339
def test_alignment():
340+
Quantity.reset_prefs()
334341
assert '<{:<16s}>'.format(Quantity('h')) == '<662.61e-36 J-s >'
335342
assert '<{:^16s}>'.format(Quantity('h')) == '< 662.61e-36 J-s >'
336343
assert '<{:>16s}>'.format(Quantity('h')) == '< 662.61e-36 J-s>'
@@ -339,6 +346,7 @@ def test_alignment():
339346
assert '<{:>17s}>'.format(Quantity('h')) == '< 662.61e-36 J-s>'
340347

341348
def test_format_method():
349+
Quantity.reset_prefs()
342350
Quantity.set_prefs(
343351
spacer = None,
344352
show_label = None,
@@ -414,6 +422,7 @@ def test_format_method():
414422
assert q.format('#,P') == 'f = 1,420,405,751.7860 Hz'
415423

416424
def test_render():
425+
Quantity.reset_prefs()
417426
Quantity.set_prefs(
418427
spacer = None,
419428
show_label = None,
@@ -484,6 +493,7 @@ def test_render():
484493
assert q.render(prec=4, strip_zeros=True, strip_radix=True) == '$123.45'
485494

486495
def test_sign():
496+
Quantity.reset_prefs()
487497
Quantity.set_prefs(spacer=None, show_label=None, label_fmt=None, label_fmt_full=None, show_desc=False)
488498

489499
# Positive numbers
@@ -735,6 +745,8 @@ def test_sign():
735745
assert '{:p}'.format(q) == '−inf Hz'
736746

737747
def test_radix_comma_output():
748+
Quantity.reset_prefs()
749+
738750
with Quantity.prefs(
739751
spacer = None,
740752
show_label = None,
@@ -777,6 +789,7 @@ def test_radix_comma_output():
777789
assert '{:,.2P}'.format(q) == 'c = 299.792.458 m/s'
778790

779791
def test_plus_minus():
792+
Quantity.reset_prefs()
780793
with Quantity.prefs(
781794
spacer = None,
782795
show_label = None,
@@ -814,6 +827,7 @@ def test_plus_minus():
814827
assert qmm.render(form='sia') == '−1 us'
815828

816829
def test_radix_comma_input():
830+
Quantity.reset_prefs()
817831
with Quantity.prefs(
818832
spacer = None,
819833
show_label = None,
@@ -851,6 +865,7 @@ def test_radix_comma_input():
851865
assert Quantity('$1.00e8').render() == '$100M'
852866

853867
def test_radix_comma_exception():
868+
Quantity.reset_prefs()
854869
with pytest.raises(ValueError) as exception:
855870
with Quantity.prefs(
856871
radix = ',',
@@ -875,6 +890,7 @@ def test_radix_comma_exception():
875890

876891

877892
def test_output_sf():
893+
Quantity.reset_prefs()
878894
with Quantity.prefs(output_sf = Quantity.all_sf):
879895
q=Quantity('c')
880896
assert f"{Quantity(1e35, 'Hz')}" == '100e33 Hz'

0 commit comments

Comments
 (0)