quantiphy 2.20__tar.gz → 2.22__tar.gz

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.
quantiphy-2.22/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016-2026 Kenneth S. Kundert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: quantiphy
3
- Version: 2.20
3
+ Version: 2.22
4
4
  Summary: physical quantities (numbers with units)
5
5
  Keywords: quantities,physical quantity,units,SI scale factors,engineering notation,mks,cgs
6
6
  Author: Ken Kundert
@@ -16,6 +16,7 @@ Classifier: Operating System :: POSIX :: Linux
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Topic :: Utilities
18
18
  Classifier: Topic :: Scientific/Engineering
19
+ License-File: LICENSE
19
20
  Project-URL: changelog, https://github.com/KenKundert/quantiphy/blob/master/doc/releases.rst
20
21
  Project-URL: documentation, https://quantiphy.readthedocs.io
21
22
  Project-URL: homepage, https://quantiphy.readthedocs.io
@@ -27,8 +28,8 @@ QuantiPhy — Physical Quantities
27
28
  |downloads| |build status| |coverage| |rtd status| |pypi version| |anaconda version| |python version|
28
29
 
29
30
  | Author: Ken Kundert
30
- | Version: 2.20
31
- | Released: 2024-04-27
31
+ | Version: 2.22
32
+ | Released: 2026-06-27
32
33
  |
33
34
 
34
35
 
@@ -102,7 +103,7 @@ Quick Start
102
103
  You can find the documentation on `ReadTheDocs
103
104
  <https://quantiphy.readthedocs.io>`_. Install with::
104
105
 
105
- pip3 install --user quantiphy
106
+ pip3 install quantiphy
106
107
 
107
108
  Requires Python 3.6 or newer. If you using an earlier version of Python,
108
109
  install version 2.10 of *QuantiPhy*.
@@ -4,8 +4,8 @@ QuantiPhy — Physical Quantities
4
4
  |downloads| |build status| |coverage| |rtd status| |pypi version| |anaconda version| |python version|
5
5
 
6
6
  | Author: Ken Kundert
7
- | Version: 2.20
8
- | Released: 2024-04-27
7
+ | Version: 2.22
8
+ | Released: 2026-06-27
9
9
  |
10
10
 
11
11
 
@@ -79,7 +79,7 @@ Quick Start
79
79
  You can find the documentation on `ReadTheDocs
80
80
  <https://quantiphy.readthedocs.io>`_. Install with::
81
81
 
82
- pip3 install --user quantiphy
82
+ pip3 install quantiphy
83
83
 
84
84
  Requires Python 3.6 or newer. If you using an earlier version of Python,
85
85
  install version 2.10 of *QuantiPhy*.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantiphy"
3
- version = "2.20"
3
+ version = "2.22"
4
4
  description = "physical quantities (numbers with units)"
5
5
  readme = "README.rst"
6
6
  keywords = [
@@ -38,3 +38,13 @@ changelog = "https://github.com/KenKundert/quantiphy/blob/master/doc/releases.rs
38
38
  [build-system]
39
39
  requires = ["flit_core >=2,<4"]
40
40
  build-backend = "flit_core.buildapi"
41
+
42
+ [tool.ruff]
43
+ exclude = [".tox", "doc", "tests", ".diffs"]
44
+
45
+ [tool.ruff.lint]
46
+ select = ["F"]
47
+ ignore = []
48
+
49
+ [tool.ruff.lint.per-file-ignores]
50
+ "quantiphy/__init__.py" = ["F401"] # imported but unused
@@ -22,7 +22,7 @@ Documentation can be found at https://quantiphy.readthedocs.io.
22
22
  """
23
23
 
24
24
  # MIT License {{{1
25
- # Copyright (C) 2016-2024 Kenneth S. Kundert
25
+ # Copyright (C) 2016-2026 Kenneth S. Kundert
26
26
  #
27
27
  # Permission is hereby granted, free of charge, to any person obtaining a copy
28
28
  # of this software and associated documentation files (the "Software"), to deal
@@ -73,21 +73,29 @@ def _scale(scale, unscaled):
73
73
  if isinstance(scale, str):
74
74
  scaled = UnitConversion._convert_units(scale, unscaled.units, unscaled)
75
75
  to_units = scale
76
- else:
77
- # otherwise, it might be a function
76
+ return scaled, to_units
77
+
78
+ if callable(scale):
78
79
  try:
79
- scaled, to_units = scale(unscaled, unscaled.units)
80
- # passing units as second argument is redundant, deprecated
81
- except TypeError:
82
- # otherwise, assume it is a scale factor
80
+ scaled, to_units = scale(unscaled)
81
+ # do not pass units as second argument, this is the new style
82
+ except TypeError as e:
83
83
  try:
84
- # might be a tuple containing scale factor and units
85
- multiplier, to_units = scale
86
- except TypeError:
87
- # otherwise, assume it is just a scale factor
88
- multiplier = scale
89
- to_units = unscaled.units
90
- scaled = multiplier * unscaled
84
+ scaled, to_units = scale(unscaled, unscaled.units)
85
+ # passing units as second argument is redundant, deprecated
86
+ except TypeError: # pragma: no cover
87
+ raise e
88
+ return scaled, to_units
89
+
90
+ # otherwise, assume it is a scale factor
91
+ try:
92
+ # might be a tuple containing scale factor and units
93
+ multiplier, to_units = scale
94
+ except TypeError:
95
+ # otherwise, assume it is just a scale factor
96
+ multiplier = scale
97
+ to_units = unscaled.units
98
+ scaled = multiplier * unscaled
91
99
  return scaled, to_units
92
100
 
93
101
 
@@ -367,8 +375,8 @@ def add_constant(value, alias=None, unit_systems=None):
367
375
 
368
376
 
369
377
  # Globals {{{1
370
- __version__ = '2.20'
371
- __released__ = '2024-04-27'
378
+ __version__ = '2.22'
379
+ __released__ = '2026-06-27'
372
380
 
373
381
  # These mappings are only used when reading numbers
374
382
  # The key for these mappings must be a single character
@@ -418,18 +426,26 @@ BINARY_MAPPINGS = {
418
426
  # These mappings are only used when writing numbers
419
427
  BIG_SCALE_FACTORS = 'kMGTPEZYRQ'
420
428
  # These must be given in order, one for every three decades.
421
- # Use k rather than K, because K looks like a temperature when used alone.
429
+ # Use k rather than K because K looks like a temperature when used alone.
422
430
 
423
431
  SMALL_SCALE_FACTORS = 'munpfazyrq'
424
432
  # These must be given in order, one for every three decades.
425
433
 
426
- # Supported currency symbols (these go on left side of number)
427
- CURRENCY_SYMBOLS = '$€¥£₩₺₽₹Ƀ₿Ξ'
434
+ # Supported currency symbols (these precede the number)
435
+ CURRENCY_SYMBOLS = '$€¥£₩₺₽₹Ƀ₿฿Ξ'
436
+
437
+ # Units that abut the number.
438
+ # % is controversial, NIST and ISO say that a space should be used to separate
439
+ # the percent sign from a number, but the Chicago Manual of Style says the
440
+ # opposite.
441
+ TIGHT_UNITS = '''°'"′″'''
442
+ # The code is written assuming that TIGHT_UNITS includes only single
443
+ # character symbols, though the user can add multi-character tight units.
428
444
 
429
445
  # Unit symbols that are not simple letters.
430
446
  # Do not include % as it will be picked up when converting text to numbers,
431
447
  # which is generally not desired (you would end up converting 0.001% to 1m%).
432
- UNIT_SYMBOLS = '°ÅΩƱΩ℧¢$€¥£₩₺₽₹Ƀ₿șΞΔ'
448
+ UNIT_SYMBOLS = """ÅΩƱΩ℧Δ¢ș""" + CURRENCY_SYMBOLS + TIGHT_UNITS
433
449
 
434
450
  # Regular expression for recognizing and decomposing string .format method codes
435
451
  FORMAT_SPEC = re.compile(r'''\A
@@ -483,6 +499,8 @@ DEFAULTS = dict(
483
499
  output_sf = 'TGMkmunpfa',
484
500
  plus = '+',
485
501
  prec = 4,
502
+ preferred_quantities = {},
503
+ _preferred_quantities = {}, # transposed version of preferred_quantities
486
504
  preferred_units = {},
487
505
  _preferred_units = {}, # transposed version of preferred_units
488
506
  radix = '.',
@@ -494,7 +512,7 @@ DEFAULTS = dict(
494
512
  spacer = ' ',
495
513
  strip_radix = True,
496
514
  strip_zeros = True,
497
- tight_units = ''' % ° ' " ′ ″ '''.split(),
515
+ tight_units = list(TIGHT_UNITS),
498
516
  unity_sf = '',
499
517
  )
500
518
 
@@ -642,35 +660,54 @@ class Quantity(float):
642
660
 
643
661
  # preferences {{{2
644
662
  _initialized = False
663
+ transparent_preferences = False
664
+ # if set true, subclasses will use current preferences from the parent
665
+ # class, even those that have changed since the subclass was created.
645
666
 
646
667
  # initialize preferences {{{3
647
668
  @classmethod
648
669
  def _initialize_preferences(cls):
649
- if cls._initialized == id(cls):
650
- return
651
- cls.reset_prefs()
670
+ if cls._initialized != id(cls):
671
+ cls.reset_prefs()
652
672
 
653
673
  # reset preferences {{{3
654
674
  @classmethod
655
- def reset_prefs(cls):
675
+ def reset_prefs(cls, transparent=None):
656
676
  """Reset preferences
657
677
 
678
+ :arg bool transparent:
679
+ If true this class inherits current preferences from the parent
680
+ classes. In false, the parents preferences are copied into this
681
+ class the first time it is used, and any changes made to the parents
682
+ preferences after first use are ignored. The default is false.
683
+
658
684
  Resets all preferences to the current preferences of the parent class.
659
685
  If there is no parent class, they are reset to their defaults.
660
686
  """
687
+ if transparent is None:
688
+ transparent = cls.transparent_preferences
689
+
661
690
  cls._initialized = id(cls)
662
691
  if cls == Quantity:
692
+ # this is the base class
663
693
  prefs = DEFAULTS
694
+ transparent = False
664
695
  else:
696
+ # this is a subclass
665
697
  parent = cls.__mro__[1]
666
698
  # for some reason I cannot get super to work right
667
699
  prefs = parent._preferences
668
- # copy dict so any changes made to parent's preferences do not affect us
669
- prefs = dict(prefs)
700
+
701
+ if not transparent:
702
+ # copy dict so subsequent changes made to parent's preferences do not affect us
703
+ prefs = dict(prefs)
704
+
705
+ cls.transparent_preferences = transparent
706
+
670
707
  cls._preferences = ChainMap({}, prefs)
671
708
  # use chain to support use of contexts
672
709
  # put empty map in as first so user never accidentally deletes or
673
- # changes one of the initial preferences
710
+ # modifies one of the initial preferences
674
711
 
675
712
  # set preferences {{{3
676
713
  @classmethod
@@ -926,12 +963,20 @@ class Quantity(float):
926
963
  *QuantiPhy* currently does not add leading plus signs to either
927
964
  mantissa or exponent, so this setting is ignored.
928
965
 
929
- :arg int prec:
930
- Default precision in digits where 0 corresponds to 1 digit. Must
931
- be nonnegative. This precision is used when the full precision is
932
- not required. Default is 4.
966
+ :arg prec:
967
+ Default precision in digits where 0 corresponds to 1 digit. Must
968
+ be a nonnegative integer or "full". This precision is used when the
969
+ full precision is not required. Default is 4.
933
970
  :type prec: int or str
934
971
 
972
+ :arg dict preferred_quantities:
973
+ A dictionary that is used when looking up the preferred quantity when
974
+ instantiating. For example, if *preferred_quantities* contains the entry:
975
+ {Decibels: “dB dBV dBA dBm dBc”} where *Decibels* is a subclass of
976
+ *Quantity*, then when instantiating a quantity with units “dB”, “dBV”,
977
+ “dBA”, “dBm”, or “dBc”, the quantity returned would have *Decibels*
978
+ as its class.
979
+
935
980
  :arg dict preferred_units:
936
981
  A dictionary that is used when looking up the preferred units when
937
982
  rendering. For example, if *preferred_units* contains the entry:
@@ -967,15 +1012,19 @@ class Quantity(float):
967
1012
 
968
1013
  :type show_label: 'f', 'a', or bool
969
1014
 
1015
+ :arg bool show_units:
1016
+ Whether the units should be included when rendering a quantity to a
1017
+ string. By default *show_units* is True.
1018
+
970
1019
  :arg str spacer:
971
1020
  The spacer text to be inserted in a string between the numeric value
972
1021
  and the scale factor when units are present. Is generally specified
973
1022
  to be '' or ' '; use the latter if you prefer a space between the
974
1023
  number and the units. Generally using ' ' makes numbers easier to
975
1024
  read, particularly with complex units, and using '' is easier to
976
- parse. You could also use a Unicode non-breaking space ' '. For
977
- your convenience, you can access a non-breaking space using
978
- :attr:`Quantity.non_breaking_space`,
1025
+ parse. Use of a non-breaking space is preferred when embedding
1026
+ numbers in prose. For your convenience, you can access a
1027
+ non-breaking spaces using :attr:`Quantity.non_breaking_space`,
979
1028
  :attr:`Quantity.narrow_non_breaking_space`, or
980
1029
  :attr:`Quantity.thin_space`.
981
1030
 
@@ -1053,13 +1102,21 @@ class Quantity(float):
1053
1102
  if isinstance(kwargs.get('known_units'), str):
1054
1103
  kwargs['known_units'] = kwargs['known_units'].split()
1055
1104
 
1105
+ # split preferred_quantities
1106
+ if 'preferred_quantities' in kwargs:
1107
+ kwargs['_preferred_quantities'] = {
1108
+ unit : quantity
1109
+ for quantity, units in kwargs['preferred_quantities'].items()
1110
+ for unit in units.split()
1111
+ }
1112
+
1056
1113
  # split preferred_units
1057
1114
  if 'preferred_units' in kwargs:
1058
- _preferred_units = {}
1059
- for preferred_unit, undesired in kwargs['preferred_units'].items():
1060
- for each in undesired.split():
1061
- _preferred_units[each] = preferred_unit
1062
- kwargs['_preferred_units'] = _preferred_units
1115
+ kwargs['_preferred_units'] = {
1116
+ unit : preferred
1117
+ for preferred, units in kwargs['preferred_units'].items()
1118
+ for unit in units.split()
1119
+ }
1063
1120
 
1064
1121
  # check for unknown output scale factors
1065
1122
  if kwargs.get('output_sf'):
@@ -1119,6 +1176,7 @@ class Quantity(float):
1119
1176
 
1120
1177
  """
1121
1178
  cls._initialize_preferences()
1179
+
1122
1180
  try:
1123
1181
  return getattr(cls, name, cls._preferences[name])
1124
1182
  except KeyError:
@@ -1134,11 +1192,15 @@ class Quantity(float):
1134
1192
  def __enter__(self):
1135
1193
  cls = self.cls
1136
1194
  cls._initialize_preferences()
1137
- cls._preferences = cls._preferences.new_child()
1195
+ cls._preferences.maps.insert(0, {})
1196
+ # do not use ChainMap.new_child() as that creates a new ChainMap,
1197
+ # orphaning the original, which could be being used by a subclass
1138
1198
  cls.set_prefs(**self.kwargs)
1139
1199
 
1140
1200
  def __exit__(self, *args):
1141
- self.cls._preferences = self.cls._preferences.parents
1201
+ self.cls._preferences.maps.pop(0)
1202
+ # do not use ChainMap.parents as that creates a new ChainMap,
1203
+ # orphaning the original, which could be being used by a subclass
1142
1204
 
1143
1205
  # now, return the context manager
1144
1206
  @classmethod
@@ -1518,7 +1580,7 @@ class Quantity(float):
1518
1580
  )
1519
1581
  )
1520
1582
  cls.embedded_e_notation_only = re.compile(
1521
- '{left_delimit}{sign}{mantissa}{exponent}{space}{smpl_units}{right_delimit}'.format(
1583
+ r'{left_delimit}{sign}{mantissa}{exponent}{space}{smpl_units}\b'.format(
1522
1584
  **locals()
1523
1585
  )
1524
1586
  )
@@ -1671,7 +1733,13 @@ class Quantity(float):
1671
1733
 
1672
1734
  # create the underlying data structure and add attributes {{{3
1673
1735
  try:
1674
- self = float.__new__(cls, number)
1736
+ preferred_quantities = cls._preferences["_preferred_quantities"]
1737
+ preferred_quantity = preferred_quantities[units]
1738
+ assert issubclass(preferred_quantity, cls)
1739
+ except (AttributeError, KeyError):
1740
+ preferred_quantity = cls
1741
+ try:
1742
+ self = float.__new__(preferred_quantity, number)
1675
1743
  except TypeError:
1676
1744
  raise InvalidNumber(number)
1677
1745
  if units:
@@ -1783,11 +1851,12 @@ class Quantity(float):
1783
1851
  are taken to the be desired units and the behavior is the same as
1784
1852
  if a string were given, except that *cls* defaults to the given
1785
1853
  subclass.
1854
+ :type scale: real, pair, function, string, or quantity
1855
+
1786
1856
  :arg class cls:
1787
1857
  Class to use for return value. If not given, the class of self is
1788
1858
  used it the units do not change, in which case :class:`Quantity` is
1789
1859
  used.
1790
- :type scale: real, pair, function, string, or quantity
1791
1860
 
1792
1861
  :raises UnknownConversion(QuantiPhyError, KeyError):
1793
1862
  A unit conversion was requested and there is no corresponding unit
@@ -1864,8 +1933,8 @@ class Quantity(float):
1864
1933
  def render(
1865
1934
  self,
1866
1935
  *,
1867
- form=None, show_units=None, prec=None, show_label=None,
1868
- strip_zeros=None, strip_radix=None, scale=None, negligible=None
1936
+ form=None, show_units=None, prec=None, show_label=None, strip_zeros=None,
1937
+ strip_radix=None, spacer=None, scale=None, negligible=None
1869
1938
  ):
1870
1939
  # description {{{3
1871
1940
  """Convert quantity to a string.
@@ -1980,6 +2049,7 @@ class Quantity(float):
1980
2049
  show_units = self.show_units if show_units is None else show_units
1981
2050
  strip_zeros = self.strip_zeros if strip_zeros is None else strip_zeros
1982
2051
  strip_radix = self.strip_radix if strip_radix is None else strip_radix
2052
+ spacer = self.spacer if spacer is None else spacer
1983
2053
  negligible = self.negligible if negligible is None else negligible
1984
2054
  units = self._preferred_units.get(self.units, self.units) if show_units else ''
1985
2055
  if prec is None:
@@ -2102,7 +2172,7 @@ class Quantity(float):
2102
2172
  # combine mantissa, scale factor, and units and return the result {{{3
2103
2173
  if sf_is_exp == 'unk':
2104
2174
  sf_is_exp = (sf == eexp)
2105
- value = self._combine(mantissa, sf, units, self.spacer, sf_is_exp)
2175
+ value = self._combine(mantissa, sf, units, spacer, sf_is_exp)
2106
2176
  return self._label(value, show_label)
2107
2177
 
2108
2178
  # fixed() {{{2
@@ -2110,7 +2180,7 @@ class Quantity(float):
2110
2180
  self,
2111
2181
  *,
2112
2182
  show_units=None, prec=None, show_label=None, show_commas=None,
2113
- strip_zeros=None, strip_radix=None, scale=None,
2183
+ strip_zeros=None, strip_radix=None, spacer=None, scale=None,
2114
2184
  ):
2115
2185
  # description {{{3
2116
2186
  """Convert quantity to fixed-point string.
@@ -2212,6 +2282,7 @@ class Quantity(float):
2212
2282
  show_commas = self.show_commas if show_commas is None else show_commas
2213
2283
  strip_zeros = self.strip_zeros if strip_zeros is None else strip_zeros
2214
2284
  strip_radix = self.strip_radix if strip_radix is None else strip_radix
2285
+ spacer = self.spacer if spacer is None else spacer
2215
2286
  units = self._preferred_units.get(self.units, self.units) if show_units else ''
2216
2287
  if prec is None:
2217
2288
  prec = self.prec
@@ -2274,13 +2345,13 @@ class Quantity(float):
2274
2345
  mantissa += '0'
2275
2346
 
2276
2347
  # combine mantissa, scale factor and units and return result {{{3
2277
- value = self._combine(mantissa, '', units, self.spacer)
2348
+ value = self._combine(mantissa, '', units, spacer)
2278
2349
  return self._label(value, show_label)
2279
2350
 
2280
2351
  # binary() {{{2
2281
2352
  def binary(
2282
2353
  self, *, show_units=None, prec=None, show_label=None,
2283
- strip_zeros=None, strip_radix=None, scale=None,
2354
+ strip_zeros=None, strip_radix=None, spacer=None, scale=None,
2284
2355
  ):
2285
2356
  # description {{{3
2286
2357
  """Convert quantity to string using binary scale factors.
@@ -2365,6 +2436,7 @@ class Quantity(float):
2365
2436
  show_units = self.show_units if show_units is None else show_units
2366
2437
  strip_zeros = self.strip_zeros if strip_zeros is None else strip_zeros
2367
2438
  strip_radix = self.strip_radix if strip_radix is None else strip_radix
2439
+ spacer = self.spacer if spacer is None else spacer
2368
2440
  units = self._preferred_units.get(self.units, self.units) if show_units else ''
2369
2441
  if prec is None:
2370
2442
  prec = self.prec
@@ -2430,7 +2502,7 @@ class Quantity(float):
2430
2502
  mantissa += '0'
2431
2503
 
2432
2504
  # combine mantissa, scale factor and units and return result {{{3
2433
- value = self._combine(mantissa, sf, units, self.spacer, sf_is_exp)
2505
+ value = self._combine(mantissa, sf, units, spacer, sf_is_exp)
2434
2506
  return self._label(value, show_label)
2435
2507
 
2436
2508
  # is_close() {{{2
@@ -2572,11 +2644,11 @@ class Quantity(float):
2572
2644
  # code {{{3
2573
2645
  match = FORMAT_SPEC.match(template)
2574
2646
  if match:
2575
- align, alt_form, width, comma, prec, ftype, units = match.groups()
2647
+ align, use_alt_form, width, comma, prec, ftype, units = match.groups()
2576
2648
  scale = units if units else None
2577
2649
  prec = int(prec) if prec else None
2578
2650
  ftype = ftype if ftype else ''
2579
- alt_form = dict(strip_zeros=False, strip_radix=False) if alt_form else {}
2651
+ alt_form = dict(strip_zeros=False, strip_radix=False) if use_alt_form else {}
2580
2652
  if ftype and ftype in 'dnu':
2581
2653
  if ftype == 'u':
2582
2654
  value = scale if scale else self.units
@@ -2629,12 +2701,12 @@ class Quantity(float):
2629
2701
  ))
2630
2702
  else:
2631
2703
  value = float(self)
2632
- value = '{0:{1}.{2}{3}}'.format(value, comma, prec, ftype)
2704
+ value = '{0:{4}{1}.{2}{3}}'.format(value, comma, prec, ftype, use_alt_form)
2633
2705
  value = self._map_leading_sign(value)
2634
2706
  value = self._map_sign(value)
2635
2707
  width = width.lstrip('0')
2636
2708
  # format function treats 0 as a padding rather than a width
2637
- if self.strip_zeros:
2709
+ if alt_form.get("strip_zeros", self.strip_zeros):
2638
2710
  if 'e' in value:
2639
2711
  mantissa, exponent = value.split('e')
2640
2712
  if '.' in mantissa:
@@ -2923,7 +2995,8 @@ class Quantity(float):
2923
2995
  end = match.start(0)
2924
2996
  number = match.group(0)
2925
2997
  try:
2926
- number = Quantity(number).render(**kwargs)
2998
+ q = cls.__new__(cls, number)
2999
+ number = q.render(**kwargs)
2927
3000
  except ValueError: # pragma: no cover
2928
3001
  # something unexpected happened
2929
3002
  # but this is not essential, so ignore it
@@ -2972,7 +3045,8 @@ class Quantity(float):
2972
3045
  end = match.start(0)
2973
3046
  number = match.group(0)
2974
3047
  try:
2975
- number = Quantity(number).render(**kwargs)
3048
+ q = cls.__new__(cls, number)
3049
+ number = q.render(**kwargs)
2976
3050
  except ValueError: # pragma: no cover
2977
3051
  # something unexpected happened
2978
3052
  # but this is not essential, so ignore it
@@ -3130,9 +3204,6 @@ add_constant(
3130
3204
  # Unit Conversions {{{1
3131
3205
  # UnitConversion class {{{2
3132
3206
  class UnitConversion(object):
3133
- _unit_conversions = {}
3134
- _known_units = set()
3135
-
3136
3207
  # description {{{3
3137
3208
  """
3138
3209
  Creates a unit converter.
@@ -3271,6 +3342,11 @@ class UnitConversion(object):
3271
3342
 
3272
3343
  """
3273
3344
 
3345
+ _unit_conversions = {}
3346
+ _known_units = set()
3347
+ _support_si_sf_scaling = True
3348
+ _support_bin_sf_scaling = True
3349
+
3274
3350
  # constructor {{{3
3275
3351
  def __init__(self, to_units, from_units, slope=1, intercept=0):
3276
3352
  self.slope = slope
@@ -3450,6 +3526,26 @@ class UnitConversion(object):
3450
3526
  cls._unit_conversions = {}
3451
3527
  cls._known_units = set()
3452
3528
 
3529
+ @classmethod
3530
+ def enable_sf_scaling(cls, si_scaling=None, bin_scaling=None):
3531
+ """By default the given or desired units in a unit conversion or scaling
3532
+ may include scale factors. This is true for both SI and binary scale
3533
+ factors. The scale factor is provided as a prefix on the units. In
3534
+ rare cases the acceptance of scale factors may create problems. You can
3535
+ use this method to disable support for interpreting scale factors in
3536
+ unit conversions.
3537
+
3538
+ si_scaling (bool):
3539
+ Enables or disables support for SI scale factor scaling.
3540
+
3541
+ bin_scaling (bool):
3542
+ Enables or disables support for binary scale factor scaling.
3543
+ """
3544
+ if si_scaling is not None:
3545
+ cls._support_si_sf_scaling = si_scaling
3546
+ if bin_scaling is not None:
3547
+ cls._support_bin_sf_scaling = bin_scaling
3548
+
3453
3549
  # fixture() {{{3
3454
3550
  @staticmethod
3455
3551
  def fixture(converter_func):
@@ -3554,6 +3650,8 @@ class UnitConversion(object):
3554
3650
  # If you want this functionality, simply use:
3555
3651
  # Quantity(value, from_units).scale(to_units)
3556
3652
 
3653
+ orig_to_units, orig_from_units = to_units, from_units
3654
+
3557
3655
  def get_converter(to_units, from_units):
3558
3656
  # handle unity scale factor conversions
3559
3657
  if (
@@ -3571,45 +3669,43 @@ class UnitConversion(object):
3571
3669
  # b. there is no scale factor on the from_units
3572
3670
  # c. there are scale factors on both the to_ and from_units
3573
3671
 
3672
+ # separate scale factor from units
3673
+ def extract_sf(units):
3674
+ # check for binary scale factor, all of which are two characters
3675
+ if cls._support_bin_sf_scaling:
3676
+ sf, unit = units[:2], units[2:]
3677
+ if sf in BINARY_MAPPINGS:
3678
+ return sf, unit, BINARY_MAPPINGS[sf]
3679
+
3680
+ # check for SI scale factor, all of which are 1 character
3681
+ if cls._support_si_sf_scaling:
3682
+ sf, unit = units[:1], units[1:]
3683
+ if sf in MAPPINGS:
3684
+ return sf, unit, float('1' + MAPPINGS[sf])
3685
+
3686
+ return None, units, 1
3687
+
3688
+ # separate scale factor from units
3574
3689
  # handle known-unit cases for to_units
3575
- to_sf = None
3576
- to_resolved = to_units in cls._known_units # case 1
3577
- if not to_resolved:
3578
- to_prefix, to_suffix = to_units[:1], to_units[1:]
3579
- to_resolved = to_prefix in ALL_SF and to_suffix in cls._known_units
3580
- if to_resolved:
3581
- to_sf, to_units = to_prefix, to_suffix # case 2
3690
+ if to_units in cls._known_units: # case 1
3691
+ to_sf, to_scale = None, 1
3692
+ else: # case 2 or 3
3693
+ to_sf, to_units, to_scale = extract_sf(to_units)
3582
3694
 
3583
3695
  # handle known-unit cases for from_units
3584
- from_sf = None
3585
- from_resolved = from_units in cls._known_units # case 1
3586
- if not from_resolved:
3587
- from_prefix, from_suffix = from_units[:1], from_units[1:]
3588
- from_resolved = (
3589
- from_prefix in ALL_SF and from_suffix in cls._known_units
3590
- )
3591
- if from_resolved:
3592
- from_sf, from_units = from_prefix, from_suffix # case 2
3593
-
3594
- # handle same-unit cases
3595
- if not to_resolved and not from_resolved: # case 3
3596
- if to_units == from_suffix and from_prefix in ALL_SF: # case 3a
3597
- from_sf, from_units = from_prefix, from_suffix
3598
- elif from_units == to_suffix and to_prefix in ALL_SF: # case 3b
3599
- to_sf, to_units = to_prefix, to_suffix
3600
- elif from_prefix in ALL_SF and to_prefix in ALL_SF: # case 3c
3601
- to_sf, to_units = to_prefix, to_suffix
3602
- from_sf, from_units = from_prefix, from_suffix
3696
+ if from_units in cls._known_units: # case 1
3697
+ from_sf, from_scale = None, 1
3698
+ else: # case 2 or 3
3699
+ from_sf, from_units, from_scale = extract_sf(from_units)
3603
3700
 
3604
- def get_sf(sf):
3605
- if sf is None:
3606
- return 1
3607
- return float('1' + MAPPINGS[sf])
3701
+ # handle unknown unit cases (to- and from- must have same units)
3702
+ if to_units == from_units: # case 3
3703
+ return to_units, from_units, to_scale, from_scale
3608
3704
 
3609
- if to_sf or from_sf:
3610
- return to_units, from_units, get_sf(to_sf), get_sf(from_sf)
3705
+ if to_units in cls._known_units or from_units in cls._known_units: # case 2
3706
+ return to_units, from_units, to_scale, from_scale
3611
3707
 
3612
- raise UnknownConversion(to_units=to_units, from_units=from_units)
3708
+ raise UnknownConversion(to_units=orig_to_units, from_units=orig_from_units)
3613
3709
 
3614
3710
  to_units, from_units, to_sf, from_sf = get_converter(to_units, from_units)
3615
3711
 
@@ -3618,8 +3714,29 @@ class UnitConversion(object):
3618
3714
  value = Quantity(value, from_units)
3619
3715
  if to_units == from_units:
3620
3716
  return from_sf * value / to_sf
3621
- converter = cls._unit_conversions[(to_units, from_units)]
3622
- return converter(value.scale(from_sf)) / to_sf
3717
+ try:
3718
+ converter = cls._unit_conversions[(to_units, from_units)]
3719
+ except KeyError:
3720
+ raise UnknownConversion(to_units=orig_to_units, from_units=orig_from_units)
3721
+ scaled = value.scale(from_sf)
3722
+ try:
3723
+ value = converter(scaled)
3724
+ except TypeError as e:
3725
+ # assume that this scale function is the deprecated form that
3726
+ # expects units as the second argument
3727
+ try:
3728
+ value = converter(scaled, scaled.units)
3729
+ except TypeError: # pragma: no cover
3730
+ raise e
3731
+ try:
3732
+ value, units = value
3733
+ # the Quantity.scale() method returns the units along with the
3734
+ # scaled value. This differs from UnitConversion scale
3735
+ # functions, which do not return the units. This code allows
3736
+ # UnitConversion to work with Quantity.scale() scale functions.
3737
+ except TypeError:
3738
+ pass
3739
+ return value / to_sf
3623
3740
 
3624
3741
 
3625
3742
  # __str__ {{{3
@@ -3649,7 +3766,7 @@ UnitConversion('K', 'F °F', 5/9, 273.15 - 32*5/9)
3649
3766
  UnitConversion('K', 'R °R', 5/9, 0)
3650
3767
 
3651
3768
  # Length/Distance conversions {{{2
3652
- UnitConversion('m', 'micron', 1/1000000)
3769
+ UnitConversion('m', 'micron microns', 1/1000000)
3653
3770
  UnitConversion('m', 'Å angstrom', 1/10000000000)
3654
3771
  UnitConversion('m', 'mi mile miles', 1609.344)
3655
3772
  UnitConversion('m', 'ft feet', 0.3048)
@@ -3666,10 +3783,12 @@ UnitConversion('s', 'hr hour hours', 3600)
3666
3783
  UnitConversion('s', 'day days', 86400)
3667
3784
 
3668
3785
  # Bit conversions {{{2
3669
- UnitConversion('b', 'B', 8)
3786
+ UnitConversion('b bit bits', 'B byte bytes', 8)
3787
+ UnitConversion('bps b/s', 'Bps B/s', 8)
3670
3788
 
3671
- # Bitcoin conversions {{{2
3672
- UnitConversion(['sat', 'sats', 'ș'], ['BTC', 'btc', 'Ƀ', '₿'], 1e8)
3789
+ # Currency conversions {{{2
3790
+ UnitConversion(['$'], ['USD'], 1)
3791
+ UnitConversion(['sat', 'sats', 'ș'], ['BTC', 'btc', 'Ƀ', '₿', '฿'], 1e8)
3673
3792
 
3674
3793
 
3675
3794
  # Quantity functions {{{1
@@ -3771,3 +3890,5 @@ def binary(value, units, params=None, *args, **kwargs):
3771
3890
 
3772
3891
  """
3773
3892
  return Quantity(value, units=units, params=params).binary(*args, **kwargs)
3893
+
3894
+ # vim: set sw=4 sts=4 tw=80 fo=ntcqwa12 et spell:
@@ -119,6 +119,7 @@ class Quantity(float):
119
119
  show_label: bool | str = ...,
120
120
  strip_zeros: bool = ...,
121
121
  strip_radix: bool = ...,
122
+ spacer: str | bool = ...,
122
123
  scale: str | float | tuple[float | Quantity, str] | Callable = ...,
123
124
  negligible: float = ...,
124
125
  ) -> str: ...
File without changes
File without changes