# Licensed under a 3-clause BSD style license - see LICENSE.rst
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from ..extern import six
import functools
import types
import warnings
from ..units.core import Unit, UnitsError
from ..units.quantity import Quantity
from ..utils import lazyproperty
from ..utils.exceptions import AstropyUserWarning
__all__ = ['Constant', 'EMConstant']
class ConstantMeta(type):
"""Metaclass for the :class:`Constant`. The primary purpose of this is to
wrap the double-underscore methods of :class:`Quantity` which is the
superclass of :class:`Constant`.
In particular this wraps the operator overloads such as `__add__` to
prevent their use with constants such as ``e`` from being used in
expressions without specifying a system. The wrapper checks to see if the
constant is listed (by name) in ``Constant._has_incompatible_units``, a set
of those constants that are defined in different systems of units are
physically incompatible. It also performs this check on each `Constant` if
it hasn't already been performed (the check is deferred until the
`Constant` is actually used in an expression to speed up import times,
among other reasons).
"""
def __new__(mcls, name, bases, d):
def wrap(meth):
@functools.wraps(meth)
def wrapper(self, *args, **kwargs):
name_lower = self.name.lower()
instances = Constant._registry[name_lower]
if not self._checked_units:
for inst in six.itervalues(instances):
try:
self.unit.to(inst.unit)
except UnitsError:
Constant._has_incompatible_units.add(name_lower)
self._checked_units = True
if (not self.system and
name_lower in Constant._has_incompatible_units):
systems = sorted([x for x in instances if x])
raise TypeError(
'Constant {0!r} does not have physically compatible '
'units across all systems of units and cannot be '
'combined with other values without specifying a '
'system (eg. {1}.{2})'.format(self.abbrev, self.abbrev,
systems[0]))
return meth(self, *args, **kwargs)
return wrapper
# The wrapper applies to so many of the __ methods that it's easier to
# just exclude the ones it doesn't apply to
exclude = set(['__new__', '__array_finalize__', '__array_wrap__',
'__dir__', '__getattr__', '__init__', '__str__',
'__repr__', '__hash__', '__iter__', '__getitem__',
'__len__', '__nonzero__'])
for attr, value in list(six.iteritems(vars(Quantity))):
if (isinstance(value, types.FunctionType) and
attr.startswith('__') and attr.endswith('__') and
attr not in exclude):
d[attr] = wrap(value)
return super(ConstantMeta, mcls).__new__(mcls, name, bases, d)
@six.add_metaclass(ConstantMeta)
[docs]class Constant(Quantity):
"""A physical or astronomical constant.
These objects are quantities that are meant to represent physical
constants.
"""
_registry = {}
_has_incompatible_units = set()
def __new__(cls, abbrev, name, value, unit, uncertainty, reference,
system=None):
name_lower = name.lower()
instances = Constant._registry.setdefault(name_lower, {})
if system in instances:
warnings.warn('Constant {0!r} is already has a definition in the '
'{1!r} system'.format(name, system), AstropyUserWarning)
inst = super(Constant, cls).__new__(cls, value)
for c in six.itervalues(instances):
if system is not None and not hasattr(c.__class__, system):
setattr(c, system, inst)
if c.system is not None and not hasattr(inst.__class__, c.system):
setattr(inst, c.system, c)
instances[system] = inst
return inst
def __init__(self, abbrev, name, value, unit, uncertainty, reference,
system=None):
self._abbrev = abbrev
self._name = name
self._value = value
self._unit = unit
self._uncertainty = uncertainty
self._reference = reference
self._system = system
self._checked_units = False
def __repr__(self):
return ('<Constant name={0!r} value={1} error={2} units={3!r} '
'reference={4!r}>'.format(self.name, self.value,
self.uncertainty, str(self.unit),
self.reference))
def __str__(self):
return (' Name = {0}\n'
' Value = {1}\n'
' Error = {2}\n'
' Units = {3}\n'
' Reference = {4}'.format(self.name, self.value,
self.uncertainty, self.unit,
self.reference))
@property
[docs] def abbrev(self):
"""A typical ASCII text abbreviation of the constant, also generally
the same as the Python variable used for this constant.
"""
return self._abbrev
@property
[docs] def name(self):
"""The full name of the constant."""
return self._name
@lazyproperty
[docs] def unit(self):
"""The unit(s) in which this constant is defined."""
return Unit(self._unit)
@property
[docs] def uncertainty(self):
"""The known uncertainty in this constant's value."""
return self._uncertainty
@property
[docs] def reference(self):
"""The source used for the value of this constant."""
return self._reference
@property
[docs] def system(self):
"""The system of units in which this constant is defined (typically
`None` so long as the constant's units can be directly converted
between systems).
"""
return self._system
@property
[docs] def si(self):
"""If the Constant is defined in the SI system return that instance of
the constant, else convert to a Quantity in the appropriate SI units.
"""
instances = Constant._registry[self.name.lower()]
return instances.get('si') or super(Constant, self).si
@property
[docs] def cgs(self):
"""If the Constant is defined in the CGS system return that instance of
the constant, else convert to a Quantity in the appropriate CGS units.
"""
instances = Constant._registry[self.name.lower()]
return instances.get('cgs') or super(Constant, self).cgs
[docs]class EMConstant(Constant):
"""An electromagnetic constant."""
@property
[docs] def cgs(self):
"""Overridden for EMConstant to raise a `TypeError` emphasizing that
there are multiple EM extensions to CGS.
"""
raise TypeError("Cannot convert EM constants to cgs because there "
"are different systems for E.M constants within the "
"c.g.s system (ESU, Gaussian, etc.). Instead, "
"directly use the constant with the appropriate "
"suffix (e.g. e.esu, e.gauss, etc.).")