# coding=utf-8
"""Descriptors, factory classes etc for the Radiance library."""
import warnings
import os
try:
basestring
from collections import Iterable
except NameError:
basestring = str
from typing import Iterable
__all__ = ['RadiancePath', 'RadianceNumber', 'RadianceBoolFlag',
'RadianceTuple', 'RadianceValue', 'RadianceReadOnly']
class RadianceDefault(object):
"""
The default descriptor for radiance commands.
Provides base case attributes for other descriptors.
Attributes:
name: Required for all cases. Name of the flag, like 'ab' for '-ab 5'
in rtrace etc. Note that some of the radiance flags are actually
keywords in python. For example -or in rcollate or -as in rtrace.
In such cases the name of the flag should be specified as orX or
asX respectively. Refer the rcollate definition for an example.
descriptive_name: This is the human-readable name of the flag. For
example 'ambient divisions' for 'ab', 'view file' for 'vf' etc.
These descriptions are usually available in the manual pages of
Radiance. Although this is an optional input, for the purposes of
debugging and readability, it is strongly suggested that this input
be specified for all instances.
accepted_inputs:Optional. List of inputs that are permissible for a
particular command option. For example, the -h flag in rcollate
only accepts 'i' or 'o' as options. So, in cases where permissible
inputs are known it is recommended that this input be specified.If
the user-specified input doesn't exist in _accepted_inputs then a
value error will be raised.
valid_range: Optional. The valid range for several prominent radiance
parameters is between 0 and 1. There are likely to be other
parameters with similar valid ranges. If _valid_range is specified,
a warning will be issued in case the provided input is not within
that range.
default_value: Optional. The value to be assigned in case no value is
assigned by the user. If the default value is not specified then
the attribute won't be considered int the creation of the
to_rad_string string representation of the component.
is_joined: Optional. A boolean that indicates if the name and value
are joined in Radiance command. For instance it should be False for
-ab 5 and should be True for -of. (Default: False)
"""
__slots__ = ('_name', '_descriptive_name', '_accepted_inputs', '_default_value',
'_valid_range', '_nameString', '_is_joined')
def __init__(self, name, descriptive_name=None, accepted_inputs=None,
valid_range=None, default_value=None, is_joined=False):
"""Init descriptor.
The constructor (__init__) initializes name, descriptive_name,
accepted_inputs and valid_range. If specified, tests if valid_range is
specified properly. Creates a readable description of the command with
the nameString attribute.
"""
self._name = "_" + name
self._descriptive_name = descriptive_name
self._accepted_inputs = accepted_inputs
self._default_value = default_value
self._is_joined = is_joined
# check if the valid range is a 2-number tuple. Sort it if it isn't
# sorted already.
if valid_range:
assert isinstance(valid_range, (tuple, list)) and len(
valid_range) == 2, \
"The input for valid_range should be a tuple/list containing" \
" expected minimum and maximum values"
valid_range = sorted(valid_range)
self._valid_range = valid_range
# create nameString.
self._nameString = "%s (%s)" % (name, descriptive_name) \
if descriptive_name \
else name
@property
def isRadianceDataType(self):
"""Check if object is a RadianceDataType."""
return True
def __get__(self, instance, owner):
"""Return value.
Raise an AttributeError through getattr if the value hasn't been
specified at all. None value has no meaning in Radiance. So, if None is
spefied as an input, then raise a standard exception. If everything is
the way it should be, then just return the value of the attribute.
"""
try:
value = getattr(instance, self._name)
except AttributeError:
if self._default_value is not None:
value = RadianceDataType(self._name, self._default_value,
self._is_joined)
else:
# create a radianceDataType with value None
# to_rad_string will return and empty string
value = RadianceDataType(self._name, None,
self._is_joined)
return value
def __set__(self, instance, value):
"""
If _accepted_inputs is specified then check if the input is among the
_accepted_inputs and assign it as an attribute. Else Raise a value
error.
"""
if value is not None:
if self._accepted_inputs:
inputs = list(self._accepted_inputs)
if value not in inputs:
raise ValueError("The value for %s should be one of the"
" following: %s. The provided value was %s"
% (self._nameString,
",".join(map(str, inputs)), value))
if instance:
setattr(instance, self._name,
RadianceDataType(self._name, value, self._is_joined))
else:
setattr(instance, self._name,
RadianceDataType(self._name, None, self._is_joined))
def __repr__(self):
"""Value representation."""
return str(self._default_value)
[docs]class RadianceValue(RadianceDefault):
"""A Radiance string value.
Attributes:
name: Required for all cases. Name of the flag, like 'ab' for '-ab 5'
in rtrace etc. Note that some of the radiance flags are actually
keywords in python. For example -or in rcollate or -as in rtrace.
In such cases the name of the flag should be specified as orX or
asX respectively. Refer the rcollate definition for an example.
descriptive_name: This is the human-readable name of the flag. For
example 'ambient divisions' for 'ab', 'view file' for 'vf' etc.
These descriptions are usually available in the manual pages of
Radiance. Although this is an optional input, for the purposes of
debugging and readability, it is strongly suggested that this input
be specified for all instances.
accepted_inputs:Optional. List of inputs that are permissible for a
particular command option. For example, the -h flag in rcollate
only accepts 'i' or 'o' as options. So, in cases where permissible
inputs are known it is recommended that this input be specified.If
the user-specified input doesn't exist in _accepted_inputs then a
value error will be raised.
default_value: Optional. The value to be assigned in case no value is
assigned by the user. If the default value is not specified then
the attribute won't be considered int the creation of the
to_rad_string string representation of the component.
is_joined: Set to True if the Boolean should be returned as a joined
output (i.e. -of, -od) (Default: False)
Usage:
o = RadianceValue('o', 'output format', default_value='f',
accepted_inputs=('f', 'd'))
"""
__slots__ = ()
def __init__(self, name, descriptive_name=None, accepted_inputs=None,
default_value=None, is_joined=False):
"""Init Radiance value."""
RadianceDefault.__init__(self, name, descriptive_name=descriptive_name,
accepted_inputs=accepted_inputs, valid_range=None,
default_value=default_value, is_joined=is_joined)
[docs]class RadianceBoolFlag(RadianceDefault):
"""This input is expected to a boolean value (i.e. True or False).
Attributes:
name: Required for all cases. Name of the flag, like 'ab' for '-ab 5'
in rtrace etc. Note that some of the radiance flags are actually
keywords in python. For example -or in rcollate or -as in rtrace.
In such cases the name of the flag should be specified as orX or
asX respectively. Refer the rcollate definition for an example.
descriptive_name: This is the human-readable name of the flag. For
example 'ambient divisions' for 'ab', 'view file' for 'vf' etc.
These descriptions are usually available in the manual pages of
Radiance. Although this is an optional input, for the purposes of
debugging and readability, it is strongly suggested that this input
be specified for all instances.
default_value: Optional. The value to be assigned in case no value is
assigned by the user. If the default value is not specified then
the attribute won't be considered int the creation of the
to_rad_string string representation of the component.
is_dual_sign: Set to True if the Boolean should return +/- value.
(i.e. +I/-I) (Default: False)
"""
__slots__ = ('_is_dual_sign',)
def __init__(self, name, descriptive_name=None, default_value=None,
is_dual_sign=False):
"""Init Boolean."""
RadianceDefault.__init__(self, name, descriptive_name=descriptive_name,
accepted_inputs=(0, 1, True, False),
valid_range=None, default_value=default_value)
# This is useful for generating radiance string
self._is_dual_sign = is_dual_sign
def __get__(self, instance, owner):
"""Return value.
Raise an AttributeError through getattr if the value hasn't been
specified at all. None value has no meaning in Radiance. So, if None is
spefied as an input, then raise a standard exception. If everything is
the way it should be, then just return the value of the attribute.
"""
try:
value = getattr(instance, self._name)
except AttributeError:
if self._default_value is not None:
value = RadianceBoolType(self._name, self._default_value,
self._is_dual_sign)
else:
value = RadianceBoolType(self._name, None,
self._is_dual_sign)
return value
def __set__(self, instance, value):
"""Overwrite set for RadianceBoolType."""
if value is not None:
if self._accepted_inputs:
inputs = list(self._accepted_inputs)
if value not in inputs:
raise ValueError("The value for %s should be one of the"
" following: %s. The provided value was %s"
% (self._nameString,
",".join(map(str, inputs)), value))
setattr(instance, self._name,
RadianceBoolType(self._name, bool(value), self._is_dual_sign))
[docs]class RadianceNumber(RadianceDefault):
"""
This input is expected to be an integer or floating point number.
Attributes:
name: Required for all cases. Name of the flag, like 'ab' for '-ab 5'
in rtrace etc. Note that some of the radiance flags are actually
keywords in python. For example -or in rcollate or -as in rtrace.
In such cases the name of the flag should be specified as orX or
asX respectively. Refer the rcollate definition for an example.
descriptive_name: This is the human-readable name of the flag. For
example 'ambient divisions' for 'ab', 'view file' for 'vf' etc.
These descriptions are usually available in the manual pages of
Radiance. Although this is an optional input, for the purposes of
debugging and readability, it is strongly suggested that this input
be specified for all instances.
accepted_inputs:Optional. List of inputs that are permissible for a
particular command option. For example, the -h flag in rcollate
only accepts 'i' or 'o' as options. So, in cases where permissible
inputs are known it is recommended that this input be specified.If
the user-specified input doesn't exist in _accepted_inputs then a
value error will be raised.
valid_range: Optional. The valid range for several prominent radiance
parameters is between 0 and 1. There are likely to be other
parameters with similar valid ranges. If _valid_range is specified,
a warning will be issued in case the provided input is not within
that range.
check_positive: Optional. Check if the number should be greater than
or equal to zero.
num_type: Optional. Acceptable inputs are float or int. If specified, the
__set__ method will ensure that the value is stored in that type.
Also, if the number changes (for example from 4.212 to 4 due to int
being specified as _type_), then a warning will be issued.
default_value: Optional. The value to be assigned in case no value is
assigned by the user. If the default value is not specified then
the attribute won't be considered int the creation of the
to_rad_string string representation of the component.
"""
__slots__ = ('_check_positive', '_type')
def __init__(self, name, descriptive_name=None, valid_range=None,
accepted_inputs=None, num_type=None, check_positive=False,
default_value=None):
RadianceDefault.__init__(self, name, descriptive_name, accepted_inputs,
valid_range, default_value)
self._check_positive = check_positive
self._type = int if num_type is None else num_type
def __set__(self, instance, value):
"""Re-iplements the __set__ method by testing the numeric input,
converting from str to float or int (if required)and checking for valid
values.
"""
# if value is None use default value
value = self._default_value if value is None else value
if value is not None:
var_name = self._nameString
try:
# Assign type if specified.
if self._type:
final_value = self._type(value)
else:
if not isinstance(value, int):
final_value = float(value)
else:
final_value = value
if self._check_positive:
msg = "The value for %s should be greater than 0." \
" The value specified was %s" % (var_name, value)
assert final_value >= 0, msg
# Value error will be raised if the input was anything else
# other than a number.
except ValueError:
msg = "The value for %s should be a number. " \
"%s was specified instead " % (var_name, value)
raise ValueError(msg)
except TypeError:
msg = "The type of input for %s should a float or int. " \
"%s was specified instead" % (var_name, value)
raise TypeError(msg)
except AttributeError:
msg = "The type of input for %s should a float or int. " \
"%s was specified instead" % (var_name, value)
raise AttributeError(msg)
# Raise a warning if the number got modified.
if self._type and final_value != self._type(value):
msg = "The expected type for %s is %s." \
"The provided input %s has been converted to %s" % \
(var_name, self._type, value, final_value)
warnings.warn(msg)
# Raise a warning if the number isn't in the valid range.
if self._valid_range:
minVal, maxVal = self._valid_range
if not (minVal <= final_value <= maxVal):
msg = "The specified input for %s is %s. This is beyond " \
"the valid range. The value for %s should be " \
"between %s and %s" % (var_name, final_value, var_name,
minVal, maxVal)
raise ValueError(msg)
setattr(instance, self._name, RadianceNumberType(self._name, final_value))
else:
setattr(instance, self._name, RadianceNumberType(self._name, None))
[docs]class RadianceTuple(RadianceDefault):
"""
This input is expected to be a numeric tuple like (0.5,0.3,0.2) etc.
(Attributes inherited from base-class are explained there.)
Attributes:
name: Required for all cases. Name of the flag, like 'ab' for '-ab 5'
in rtrace etc. Note that some of the radiance flags are actually
keywords in python. For example -or in rcollate or -as in rtrace.
In such cases the name of the flag should be specified as orX or
asX respectively. Refer the rcollate definition for an example.
descriptive_name: This is the human-readable name of the flag. For
example 'ambient divisions' for 'ab', 'view file' for 'vf' etc.
These descriptions are usually available in the manual pages of
Radiance. Although this is an optional input, for the purposes of
debugging and readability, it is strongly suggested that this input
be specified for all instances.
accepted_inputs:Optional. List of inputs that are permissible for a
particular command option. For example, the -h flag in rcollate
only accepts 'i' or 'o' as options. So, in cases where permissible
inputs are known it is recommended that this input be specified.If
the user-specified input doesn't exist in _accepted_inputs then a
value error will be raised.
valid_range: Optional. The valid range for several prominent radiance
parameters is between 0 and 1. There are likely to be other
parameters with similar valid ranges. If _valid_range is specified,
a warning will be issued in case the provided input is not within
that range.
tuple_size: Optional. Specify the number of inputs that are expected.
num_type: Optional. Acceptable inputs are float or int. If specified, the
__set__ method will ensure that the value is stored in that type.
default_value: Optional. The value to be assigned in case no value is
assigned by the user. If the default value is not specified then
the attribute won't be considered int the creation of the
to_rad_string string representation of the component.
"""
__slots__ = ('_tuple_size', '_type', '_test_type')
def __init__(self, name, descriptive_name=None, valid_range=None,
accepted_inputs=None, tuple_size=None, num_type=None,
default_value=None, test_type=True):
RadianceDefault.__init__(self, name, descriptive_name, accepted_inputs,
valid_range, default_value)
self._tuple_size = tuple_size
self._type = num_type
self._test_type = test_type
def __set__(self, instance, value):
"""
Check for tuple size and valid range if specified.
Parse from strings if input is specified as a string instead of a number.
"""
if value is not None:
if self._test_type:
if self._type:
num_type = self._type
else:
num_type = float
try:
final_value = value.replace(',', ' ').split()
except AttributeError:
final_value = value
try:
if self._test_type:
final_value = [num_type(x) for x in final_value]
except TypeError:
msg = "The specified input for %s is %s. " \
"The value should be a list or a tuple." \
% (self._nameString, final_value)
raise ValueError(msg)
if self._tuple_size:
assert len(final_value) == self._tuple_size, \
"The number of inputs required for %s are %s. " \
"The provided input was %s" % \
(self._nameString, self._tuple_size, final_value)
if self._valid_range:
minVal, maxVal = self._valid_range
allin_range = True
for numValue in final_value:
if not (minVal <= numValue <= maxVal):
allin_range = False
break
if not allin_range:
msg = "The specified input for %s is %s. " \
"One or more numbers are not in the valid range" \
". The values should be between %s and %s" \
% (self._nameString, final_value, maxVal, minVal)
raise ValueError(msg)
setattr(instance, self._name,
RadianceDataType(self._name, tuple(final_value)))
def __getitem__(self, i):
"""Get item i from tuple."""
return self._default_value[i]
[docs]class RadiancePath(RadianceDefault):
"""
This input is expected to be a file path.
(Attributes inherited from base-class are explained there.)
Attributes:
name: Required for all cases. Name of the flag, like 'ab' for '-ab 5'
in rtrace etc. Note that some of the radiance flags are actually
keywords in python. For example -or in rcollate or -as in rtrace.
In such cases the name of the flag should be specified as orX or
asX respectively. Refer the rcollate definition for an example.
descriptive_name: This is the human-readable name of the flag. For
example 'ambient divisions' for 'ab', 'view file' for 'vf' etc.
These descriptions are usually available in the manual pages of
Radiance. Although this is an optional input, for the purposes of
debugging and readability, it is strongly suggested that this input
be specified for all instances.
relative_path: Optional. Start folder for relative path. Default is None
which returns absolute path.
check_exists: Optional. Check if the file exists. Useful in the case of
input files such as epw files etc. where it is essential for those
files to exist before the command executes.
extension: Optional. Test the extension of the file.
"""
__slots__ = ('_relative_path', '_check_exists', '_extension')
def __init__(self, name, descriptive_name=None, relative_path=None,
check_exists=False, extension=None):
"""Init path descriptor."""
RadianceDefault.__init__(self, name, descriptive_name)
self._relative_path = relative_path
self._check_exists = check_exists
self._extension = extension
def __set__(self, instance, value):
"""Set the value.
Run tests based on _expandRelative, _check_exists and _extension before
assigning the value to attribute.
"""
if value is not None:
value = str(value)
assert isinstance(value, str), \
"The input for %s should be string containing the path name." \
" %s %s was provided instead" % (self._nameString, value, type(value))
if self._check_exists:
if not os.path.exists(value):
raise IOError(
"The specified path for %s was not found in %s" % (
self._nameString, value))
if self._extension:
assert value.lower().endswith(self._extension.lower()), \
"The accepted extension for %s is %s. The provided input" \
"was %s" % (self._nameString, self._extension, value)
setattr(instance, self._name,
RadiancePathType(self._name, value, self._relative_path))
class RadianceDataType(object):
"""Base type for all Radiance types."""
__slots__ = ('_name', '_value', '_is_joined')
def __init__(self, name, value, is_joined=False):
self._name = name.replace("_", "")
self._value = value
self._is_joined = is_joined
def to_rad_string(self):
"""Return formatted value for Radiance based on the type of descriptor."""
if self._value is None:
return ""
try:
if not isinstance(self._value, basestring) \
and isinstance(self._value, Iterable):
# tuple
return "-%s %s" % (self._name, " ".join(map(str, self._value)))
else:
if self._is_joined:
# joined strings such as -of
return "-%s%s" % (self._name, str(self._value))
else:
# numbers
return "-%s %s" % (self._name, str(self._value))
except TypeError:
raise ValueError("Failed to set the value to {}".format(self._value))
def __str__(self):
return str(self._value)
def __repr__(self):
return str(self._value)
def __eq__(self, other):
return self._value == other
def __ne__(self, other):
return self._value != other
def __getitem__(self, i):
try:
return self._value[i]
except Exception as e:
raise Exception(e)
class RadianceBoolType(RadianceDataType):
"""Radiance boolean."""
__slots__ = ("_is_dual_sign",)
def __init__(self, name, value, is_dual_sign):
RadianceDataType.__init__(self, name, value)
self._is_dual_sign = is_dual_sign
def to_rad_string(self):
"""Return formatted value for Radiance based on the type of descriptor."""
if self._value is None:
return ""
try:
if self._is_dual_sign:
output = "+%s" % self._name if self._value is True \
else "-%s" % self._name
else:
output = "-%s" % self._name if self._value is True else ""
return output
except TypeError:
raise ValueError("Failed to set the value to {}".format(self._value))
def __int__(self):
return int(self._value)
def __float__(self):
return float(self._value)
def __eq__(self, other):
return self._value == float(other)
def __ne__(self, other):
return self._value != float(other)
def __lt__(self, other):
return self._value < other
def __gt__(self, other):
return self._value > other
def __le__(self, other):
return self._value <= other
def __ge__(self, other):
return self._value >= other
def __add__(self, other):
return self._value + other
def __sub__(self, other):
return self._value - other
def __mul__(self, other):
return self._value * other
def __floordiv__(self, other):
return self._value // other
def __div__(self, other):
return self._value / other
def __mod__(self, other):
return self._value % other
def __pow__(self, other):
return self._value ** other
def __radd__(self, other):
return self.__add__(other)
def __rsub__(self, other):
return other - self._value
def __rmul__(self, other):
return self.__mul__(other)
def __rfloordiv__(self, other):
return other // self._value
def __rdiv__(self, other):
return other / self._value
def __rmod__(self, other):
return other % self._value
def __rpow__(self, other):
return other ** self._value
def __nonzero__(self):
return bool(self._value)
def __bool__(self):
# python 3
return bool(self._value)
class RadiancePathType(RadianceDataType):
"""Radiance path."""
__slots__ = ("relPath",)
def __init__(self, name, value, relative_path=None):
RadianceDataType.__init__(self, name, value)
self.relPath = relative_path
"""Start folder that relative path should be calculated from.
If None absolute path will be returned.
"""
def to_rad_string(self):
"""Return formatted value for Radiance based on the type of descriptor."""
if self._value is None:
return ""
try:
if self.relPath:
return os.path.relpath(self._value, self.relPath)
else:
return os.path.normpath(self._value)
except TypeError:
raise ValueError("Failed to set the value to {}".format(self._value))
@property
def normpath(self):
if self._value is None:
return None
else:
return os.path.normpath(self._value)
class RadianceNumberType(RadianceDataType):
"""Radiance number."""
__slots__ = ()
def __init__(self, name, value):
RadianceDataType.__init__(self, name, value)
def __int__(self):
return int(self._value)
def __float__(self):
return float(self._value)
def __eq__(self, other):
return self._value == float(other)
def __ne__(self, other):
return self._value != float(other)
def __lt__(self, other):
return self._value < other
def __gt__(self, other):
return self._value > other
def __le__(self, other):
return self._value <= other
def __ge__(self, other):
return self._value >= other
def __add__(self, other):
return self._value + other
def __sub__(self, other):
return self._value - other
def __mul__(self, other):
return self._value * other
def __floordiv__(self, other):
return self._value // other
def __div__(self, other):
return self._value / other
def __mod__(self, other):
return self._value % other
def __pow__(self, other):
return self._value ** other
def __radd__(self, other):
return self.__add__(other)
def __rsub__(self, other):
return other - self._value
def __rmul__(self, other):
return self.__mul__(other)
def __rfloordiv__(self, other):
return other // self._value
def __rdiv__(self, other):
return other / self._value
def __rmod__(self, other):
return other % self._value
def __rpow__(self, other):
return other ** self._value
[docs]class RadianceReadOnly(object):
"""A descriptor for creating Readonly values."""
def __init__(self, name):
self._name = '_' + str(name)
def __get__(self, instance, owner):
return getattr(instance, self._name)
def __set__(self, instance, value):
# Let the value be set first time through constructor.
# Block all other attempts.
try:
value = getattr(instance, self._name)
raise Exception("The attribute %s is read only. "
"It's default value is %s" % (self._name[1:], value))
except AttributeError:
setattr(instance, self._name, value)