Coverage for pygeodesy / unitsBase.py: 94%
122 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-10-21 16:43 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-10-21 16:43 -0400
2# -*- coding: utf-8 -*-
4u'''Basic C{Float}, C{Int} and C{Str}ing units classes.
5'''
7from pygeodesy.basics import _isin, isstr, issubclassof, _xsubclassof, typename
8# from pygeodesy.constants import isneg0 # _MODS
9from pygeodesy.errors import _IsnotError, _UnexpectedError, UnitError, _XError
10# from pygeodesy.internals import typename # from .basics
11from pygeodesy.interns import NN, _degrees_, _degrees2_, _invalid_, _meter_, \
12 _radians_, _radians2_, _radius_, _UNDER_, _units_, \
13 _std_ # PYCHOK used!
14from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
15from pygeodesy.named import modulename, _Named, property_doc_, property_RO
16# from pygeodesy.props import property_doc_, property_RO # from .named
17from pygeodesy.streprs import Fmt, fstr
19__all__ = _ALL_LAZY.unitsBase
20__version__ = '26.02.22'
23class _NamedUnit(_Named):
24 '''(INTERNAL) Base class for C{units}.
25 '''
26 _std_repr = True # set below
27 _units = None
29 def __new__(cls, typ, arg, name, Error=UnitError, **name_arg):
30 '''(INTERNAL) Return a named C{typ.__new__(cls, arg)} instance.
31 '''
32 if name_arg:
33 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
34 try: # assert typ in cls.__mro__
35 self = typ.__new__(cls, arg)
36 if name:
37 self.name = name
38 except Exception as x:
39 raise _NamedUnit._Error(cls, arg, name, Error, cause=x)
40 return self
42 @staticmethod
43 def _arg_name_arg2(arg, name=NN, name__=None, **name_arg): # in .units
44 '''(INTERNAL) Get the 2-tuple C{(name, arg)}.
45 '''
46 if name_arg:
47 if len(name_arg) > 1:
48 raise _UnexpectedError(**name_arg)
49 for name, arg in name_arg.items(): # next(iter(.items()))
50 break
51 elif name:
52 pass
53 elif name__ is not None:
54 name = typename(name__)
55 return name, arg
57 @staticmethod # PYCHOK unused suffix
58 def _Error(cls, arg, name, Error=UnitError, # PYLINT W0211 unused
59 suffix=NN, # PYLINT W0613
60 txt=_invalid_, cause=None, **name_arg):
61 '''(INTERNAL) Return a C{_NamedUnit} error with explanation.
63 @returns: An B{C{Error}} instance.
64 '''
65 kwds, x = {}, cause
66 if x is not None: # caught exception
67 if Error is UnitError: # and isError(x)
68 Error = type(x) # i.e. not overridden
69 if txt is _invalid_:
70 txt = str(x) # i.e. not overridden
71 kwds.update(cause=x)
72 if name_arg:
73 try:
74 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
75 except Exception: # PYLINT W0718 ignore, same error?
76 kwds.update(name_arg)
77 n = name if name else modulename(cls).lstrip(_UNDER_)
78 return _XError(Error, n, arg, txt=txt, **kwds)
80 @property_doc_(' standard C{repr} or named C{toRepr} representation.')
81 def std_repr(self):
82 '''Get the representation (C{bool}, C{True} means standard).
83 '''
84 return self._std_repr
86 @std_repr.setter # PYCHOK setter!
87 def std_repr(self, std):
88 '''Set the representation (C{True} or C{"std"} for standard).
89 '''
90 self._std_repr = _isin(std, True, _std_)
92 def _toRepr(self, value):
93 '''(INTERNAL) Representation "<name> (<value>)" or "<classname>(<value>)".
94 '''
95 return (Fmt.PARENSPACED(self.name, value) if self.name else # PYLINT W0125
96 Fmt.PAREN( self.classname, value))
98 @property_doc_(' units name.')
99 def units(self):
100 '''Get the units name (C{str}).
101 '''
102 if self._units is None:
103 self._units = self.classname
104 return self._units
106 @units.setter # PYCHOK setter!
107 def units(self, units):
108 '''Set the units name for this instance (C{str} or C{None} for default).
109 '''
110 self._units = None if units is None else str(units)
113class Float(float, _NamedUnit):
114 '''Named C{float}.
115 '''
116 # _std_repr = True # set below
118 def __new__(cls, arg=None, name=NN, **Error_name_arg):
119 '''New, named C{Ffloat}.
121 @kwarg arg: The value (any C{type} acceptable to C{float}).
122 @kwarg name: Optional name (C{str}).
123 @kwarg Error_name_arg: Optional C{B{Error}=UnitError} to raise
124 and optional C{name=arg} keyword argument, inlieu
125 of separate B{C{arg}} and B{C{name}} ones.
127 @returns: A named C{Float}.
129 @raise Error: Invalid B{C{arg}}.
130 '''
131 return _NamedUnit.__new__(cls, float, arg, name, **Error_name_arg)
133 def __repr__(self): # to avoid MRO(float)
134 '''Return a representation of this C{Float}.
136 @see: Method C{Float.toRepr} and property C{Float.std_repr}.
138 @note: Use C{env} variable C{PYGEODESY_FLOAT_STD_REPR=std} prior
139 to C{import pygeodesy} to get the standard C{repr} or set
140 property C{std_repr=False} to always get the named C{toRepr}
141 representation.
142 '''
143 return self.toRepr(std=self._std_repr)
145 def __str__(self): # to avoid MRO(float)
146 '''Return this C{Float} as standard C{str}.
147 '''
148 # must use super(Float, self)... since super()... only works
149 # for Python 3+ and float.__str__(self) invokes .__repr__(self);
150 # calling self.toRepr(std=True) super(Float, self).__repr__()
151 # mimicks this bhavior
153 # XXX the default number of decimals is 10-12 when using
154 # float.__str__(self) with both python 3.8+ and 2.7-, but
155 # float.__repr__(self) shows DIG decimals in python2.7!
156 # return super(Float, self).__repr__() # see .testCss.py
157 return float.__str__(self) # always _std_str_
159 @property_RO
160 def isneg0(self):
161 '''Is this value L{NEG0<pygeodesy.NEG0>}, C{-0.0} or C{-0}? (C{bool}).
162 '''
163 return _MODS.constants.isneg0(self)
165 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
166 '''Return a representation of this C{Float}.
168 @kwarg std: If C{True}, return the standard C{repr},
169 otherwise the named representation (C{bool}).
171 @see: Function L{fstr<pygeodesy.streprs.fstr>} and methods
172 L{Float.__repr__}, L{Float.toStr} for further details.
173 '''
174 # must use super(Float, self)... since super()... only works for
175 # Python 3+; return super(Float, self).__repr__() if std else \
176 return float.__repr__(self) if std else \
177 self._toRepr(self.toStr(**prec_fmt_ints))
179 def toStr(self, prec=12, fmt=Fmt.g, ints=False): # PYCHOK prec=8, ...
180 '''Format this C{Float} as C{str}.
182 @see: Function L{fstr<pygeodesy.streprs.fstr>} and method
183 L{Float.__repr__} and for further information.
184 '''
185 return fstr(self, prec=prec, fmt=fmt, ints=ints)
188class Int(int, _NamedUnit):
189 '''Named C{int}.
190 '''
191 # _std_repr = True # set below
193 def __new__(cls, arg=None, name=NN, **Error_name_arg):
194 '''New, named C{Int}.
196 @kwarg arg: The value (any C{type} acceptable to C{int}).
197 @kwarg name: Optional name (C{str}).
198 @kwarg Error_name_arg: Optional C{B{Error}=UnitError} to raise
199 and optional C{name=arg} keyword argument, inlieu
200 of separate B{C{arg}} and B{C{name}} ones.
202 @returns: A named C{Int}.
204 @raise Error: Invalid B{C{arg}}.
205 '''
206 return _NamedUnit.__new__(cls, int, arg, name, **Error_name_arg)
208 def __repr__(self): # to avoid MRO(int)
209 '''Return a representation of this named C{int}.
211 @see: Method C{Int.toRepr} and property C{Int.std_repr}.
213 @note: Use C{env} variable C{PYGEODESY_INT_STD_REPR=std}
214 prior to C{import pygeodesy} to get the standard
215 C{repr} or set property C{std_repr=False} to always
216 get the named C{toRepr} representation.
217 '''
218 return self.toRepr(std=self._std_repr)
220 def __str__(self): # to avoid MRO(int)
221 '''Return this C{Int} as standard C{str}.
222 '''
223 return self.toStr()
225 def toRepr(self, std=False, **unused): # PYCHOK **unused
226 '''Return a representation of this C{Int}.
228 @kwarg std: If C{True}, return the standard C{repr},
229 otherwise the named representation (C{bool}).
231 @see: Method L{Int.__repr__} for more documentation.
232 '''
233 r = int.__repr__(self) # self.toStr()
234 return r if std else self._toRepr(r)
236 def toStr(self, **unused): # PYCHOK **unused
237 '''Return this C{Int} as standard C{str}.
239 @see: Method L{Int.__repr__} for more documentation.
240 '''
241 # XXX must use '%d' % (self,) since
242 # int.__str__(self) fails with 3.8+
243 return '%d' % (self,)
246class Radius(Float):
247 '''Named C{float} representing a radius, conventionally in C{meter}.
248 '''
249 def __new__(cls, arg=None, name=_radius_, **Error_name_arg):
250 '''New L{Radius} instance, see L{Float}.
251 '''
252 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
255class Str(str, _NamedUnit):
256 '''Named, callable C{str}.
257 '''
258 # _std_repr = True # set below
260 def __new__(cls, arg=None, name=NN, **Error_name_arg):
261 '''New, named and callable C{Str}.
263 @kwarg cls: This class (C{Str} or sub-class).
264 @kwarg arg: The value (any C{type} acceptable to C{str}).
265 @kwarg name: Optional name (C{str}).
266 @kwarg Error_name_arg: Optional C{B{Error}=UnitError} to raise
267 and optional C{name=arg} keyword argument, inlieu
268 of separate B{C{arg}} and B{C{name}} ones.
270 @returns: A named L{Str}.
272 @raise Error: Invalid B{C{arg}}.
274 @see: Callable, not nameable class L{Str_<pygeodesy.interns.Str_>}.
275 '''
276 return _NamedUnit.__new__(cls, str, arg, name, **Error_name_arg)
278 def __repr__(self):
279 '''Return a representation of this C{Str}.
281 @see: Method C{Str.toRepr} and property C{Str.std_repr}.
283 @note: Use C{env} variable C{PYGEODESY_STR_STD_REPR=std}
284 prior to C{import pygeodesy} to get the standard
285 C{repr} or set property C{std_repr=False} to always
286 get the named C{toRepr} representation.
287 '''
288 return self.toRepr(std=self._std_repr) # see .test/testGars.py
290 def __str__(self):
291 '''Return this C{Str} as standard C{str}.
292 '''
293 return self.toStr()
295 def join_(self, *args, **name_Error):
296 '''Join all positional B{C{args}} like C{self.join(B{args})}.
298 @return: All B{C{args}} joined by this instance (L{Str_}).
300 @note: An other L{Str} instance is returned to make the
301 result re-callable.
302 '''
303 return Str(str.join(self, map(str, args)), **name_Error) # re-callable
305 __call__ = join_
307 def toRepr(self, std=False, **unused): # PYCHOK **unused
308 '''Return a representation of this C{Str}.
310 @kwarg std: If C{True}, return the standard C{repr},
311 otherwise the named representation (C{bool}).
313 @see: Method L{Str.__repr__} for more documentation.
314 '''
315 # must use super(Str, self)... since super()... only works
316 # for Python 3+ and str.__repr__(self) fails in Python 3.8+
317 r = super(Str, self).__repr__()
318 return r if std else self._toRepr(r)
320 def toStr(self, **unused): # PYCHOK **unused
321 '''Return this C{Str} as standard C{str}.
322 '''
323 # must use super(Str, self)... since super()... only works
324 # for Python 3+ and str.__repr__(self) fails in Python 3.8+
325 return super(Str, self).__str__()
328_Str_degrees = Str(_degrees_) # PYCHOK in .frechet, .hausdorff
329_Str_degrees2 = Str(_degrees2_) # PYCHOK in .frechet, .hausdorff
330_Str_meter = Str(_meter_) # PYCHOK in .frechet, .hausdorff
331_Str_NN = Str(NN) # PYCHOK in .frechet, .hausdorff
332_Str_radians = Str(_radians_) # PYCHOK in .frechet, .hausdorff
333_Str_radians2 = Str(_radians2_) # PYCHOK in .frechet, .hausdorff
336def _xUnit(units, Base): # PYCHOK in .frechet, .hausdorff
337 '''(INTERNAL) Get C{Unit} from C{units} or C{name}, ortherwise C{Base}.
338 '''
339 _xsubclassof(_NamedUnit, Base=Base)
340 U = getattr(_MODS, units.capitalize(), Base) if isstr(units) else units
341 return U if issubclassof(U, Base) else Base
344def _xUnits(units, Base=_NamedUnit): # in .frechet, .hausdorff
345 '''(INTERNAL) Set property C{units} as C{Unit} or C{Str}.
346 '''
347 _xsubclassof(_NamedUnit, Base=Base)
348 if issubclassof(units, Base):
349 U = units
350 elif isstr(units):
351 U = Str(units, name=_units_) # XXX Str to _Pass and for backward compatibility
352 else:
353 raise _IsnotError(Base, Str, str, units=units)
354 return U
357__all__ += _ALL_DOCS(_NamedUnit)
359# **) MIT License
360#
361# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
362#
363# Permission is hereby granted, free of charge, to any person obtaining a
364# copy of this software and associated documentation files (the "Software"),
365# to deal in the Software without restriction, including without limitation
366# the rights to use, copy, modify, merge, publish, distribute, sublicense,
367# and/or sell copies of the Software, and to permit persons to whom the
368# Software is furnished to do so, subject to the following conditions:
369#
370# The above copyright notice and this permission notice shall be included
371# in all copies or substantial portions of the Software.
372#
373# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
374# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
375# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
376# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
377# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
378# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
379# OTHER DEALINGS IN THE SOFTWARE.