236 lines
8.3 KiB
Python
236 lines
8.3 KiB
Python
# size.py
|
|
# Python module to represent storage sizes
|
|
#
|
|
# Copyright (C) 2010 Red Hat, Inc.
|
|
#
|
|
# This copyrighted material is made available to anyone wishing to use,
|
|
# modify, copy, or redistribute it subject to the terms and conditions of
|
|
# the GNU General Public License v.2, or (at your option) any later version.
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY expressed or implied, including the implied warranties of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details. You should have received a copy of the
|
|
# GNU General Public License along with this program; if not, write to the
|
|
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
|
|
# source code or documentation are not subject to the GNU General Public
|
|
# License and may only be used or replicated with the express permission of
|
|
# Red Hat, Inc.
|
|
#
|
|
# Red Hat Author(s): David Cantrell <dcantrell@redhat.com>
|
|
|
|
import re
|
|
|
|
from decimal import Decimal
|
|
from decimal import InvalidOperation
|
|
|
|
from errors import *
|
|
|
|
import gettext
|
|
_ = lambda x: gettext.ldgettext("anaconda", x)
|
|
P_ = lambda x, y, z: gettext.ldngettext("anaconda", x, y, z)
|
|
|
|
# Decimal prefixes for different size increments, along with the name
|
|
# and accepted abbreviation for the prefix. These prefixes are all
|
|
# for 'bytes'.
|
|
_decimalPrefix = [(1000, _("kilo"), _("k")),
|
|
(1000**2, _("mega"), _("M")),
|
|
(1000**3, _("giga"), _("G")),
|
|
(1000**4, _("tera"), _("T")),
|
|
(1000**5, _("peta"), _("P")),
|
|
(1000**6, _("exa"), _("E")),
|
|
(1000**7, _("zetta"), _("Z")),
|
|
(1000**8, _("yotta"), _("Y"))]
|
|
|
|
# Binary prefixes for the different size increments. Same structure
|
|
# as the above list.
|
|
_binaryPrefix = [(1024, _("kibi"), _("Ki")),
|
|
(1024**2, _("mebi"), _("Mi")),
|
|
(1024**3, _("gibi"), _("Gi")),
|
|
(1024**4, _("tebi"), None),
|
|
(1024**5, _("pebi"), None),
|
|
(1024**6, _("ebi"), None),
|
|
(1024**7, _("zebi"), None),
|
|
(1024**8, _("yobi"), None)]
|
|
|
|
_bytes = [_('b'), _('byte'), _('bytes')]
|
|
_prefixes = _decimalPrefix + _binaryPrefix
|
|
|
|
def _makeSpecs(prefix, abbr):
|
|
""" Internal method used to generate a list of specifiers. """
|
|
specs = []
|
|
|
|
if prefix:
|
|
specs.append(prefix.lower() + _("byte"))
|
|
specs.append(prefix.lower() + _("bytes"))
|
|
|
|
if abbr:
|
|
specs.append(abbr.lower() + _("b"))
|
|
|
|
return specs
|
|
|
|
def _parseSpec(spec):
|
|
""" Parse string representation of size. """
|
|
if not spec:
|
|
raise ValueError("invalid size specification", spec)
|
|
|
|
m = re.match(r'([0-9.]+)\s*([A-Za-z]*)$', spec.strip())
|
|
if not m:
|
|
raise ValueError("invalid size specification", spec)
|
|
|
|
try:
|
|
size = Decimal(m.groups()[0])
|
|
except InvalidOperation:
|
|
raise ValueError("invalid size specification", spec)
|
|
|
|
if size < 0:
|
|
raise SizeNotPositiveError("spec= param must be >=0")
|
|
|
|
specifier = m.groups()[1].lower()
|
|
if specifier in _bytes or not specifier:
|
|
return size
|
|
|
|
for factor, prefix, abbr in _prefixes:
|
|
check = _makeSpecs(prefix, abbr)
|
|
|
|
if specifier in check:
|
|
return size * factor
|
|
|
|
raise ValueError("invalid size specification", spec)
|
|
|
|
class Size(Decimal):
|
|
""" Common class to represent storage device and filesystem sizes.
|
|
Can handle parsing strings such as 45MB or 6.7GB to initialize
|
|
itself, or can be initialized with a numerical size in bytes.
|
|
Also generates human readable strings to a specified number of
|
|
decimal places.
|
|
"""
|
|
|
|
def __new__(cls, bytes=None, spec=None):
|
|
""" Initialize a new Size object. Must pass either bytes or spec,
|
|
but not both. The bytes parameter is a numerical value for
|
|
the size this object represents, in bytes. The spec parameter
|
|
is a string specification of the size using any of the size
|
|
specifiers in the _decimalPrefix or _binaryPrefix lists combined
|
|
with a 'b' or 'B'. For example, to specify 640 kilobytes, you
|
|
could pass any of these parameter:
|
|
|
|
spec="640kb"
|
|
spec="640 kb"
|
|
spec="640KB"
|
|
spec="640 KB"
|
|
spec="640 kilobytes"
|
|
|
|
If you want to use spec to pass a bytes value, you can use the
|
|
letter 'b' or 'B' or simply leave the specifier off and bytes
|
|
will be assumed.
|
|
"""
|
|
if bytes and spec:
|
|
raise SizeParamsError("only specify one parameter")
|
|
|
|
if bytes is not None:
|
|
if type(bytes).__name__ in ["int", "long", "float", 'Decimal'] and bytes >= 0:
|
|
self = Decimal.__new__(cls, value=bytes)
|
|
else:
|
|
raise SizeNotPositiveError("bytes= param must be >=0")
|
|
elif spec:
|
|
self = Decimal.__new__(cls, value=_parseSpec(spec))
|
|
else:
|
|
raise SizeParamsError("missing bytes= or spec=")
|
|
|
|
return self
|
|
|
|
def __str__(self, context=None):
|
|
return self.humanReadable()
|
|
|
|
def __repr__(self):
|
|
return "Size('%s')" % self
|
|
|
|
def __add__(self, other, context=None):
|
|
return Size(bytes=Decimal.__add__(self, other, context=context))
|
|
|
|
# needed to make sum() work with Size arguments
|
|
def __radd__(self, other, context=None):
|
|
return Size(bytes=Decimal.__radd__(self, other, context=context))
|
|
|
|
def __sub__(self, other, context=None):
|
|
# subtraction is implemented using __add__ and negation, so we'll
|
|
# be getting passed a Size
|
|
return Decimal.__sub__(self, other, context=context)
|
|
|
|
def __mul__(self, other, context=None):
|
|
return Size(bytes=Decimal.__mul__(self, other, context=context))
|
|
|
|
def __div__(self, other, context=None):
|
|
return Size(bytes=Decimal.__div__(self, other, context=context))
|
|
|
|
def _trimEnd(self, val):
|
|
""" Internal method to trim trailing zeros. """
|
|
val = re.sub(r'(\.\d*?)0+$', '\\1', val)
|
|
while val.endswith('.'):
|
|
val = val[:-1]
|
|
|
|
return val
|
|
|
|
def convertTo(self, spec="b"):
|
|
""" Return the size in the units indicated by the specifier. The
|
|
specifier can be prefixes from the _decimalPrefix and
|
|
_binaryPrefix lists combined with 'b' or 'B' for abbreviations)
|
|
or 'bytes' (for prefixes like kilo or mega). The size is
|
|
returned as a Decimal.
|
|
"""
|
|
spec = spec.lower()
|
|
|
|
if spec in _bytes:
|
|
return self
|
|
|
|
for factor, prefix, abbr in _prefixes:
|
|
check = _makeSpecs(prefix, abbr)
|
|
|
|
if spec in check:
|
|
return Decimal(self / Decimal(factor))
|
|
|
|
return None
|
|
|
|
def humanReadable(self, places=None, max_places=2):
|
|
""" Return a string representation of this size with appropriate
|
|
size specifier and in the specified number of decimal places
|
|
(default: auto with a maximum of 2 decimal places).
|
|
"""
|
|
if places is not None and places < 0:
|
|
raise SizePlacesError("places= must be >=0 or None")
|
|
|
|
if max_places is not None and max_places < 0:
|
|
raise SizePlacesError("max_places= must be >=0 or None")
|
|
|
|
check = self._trimEnd("%d" % self)
|
|
|
|
if Decimal(check) < 1000:
|
|
return "%s B" % check
|
|
|
|
for factor, prefix, abbr in _prefixes:
|
|
newcheck = super(Size, self).__div__(Decimal(factor))
|
|
|
|
if newcheck < 1000:
|
|
# nice value, use this factor, prefix and abbr
|
|
break
|
|
|
|
if places is not None:
|
|
fmt = "%%.%df" % places
|
|
retval = fmt % newcheck
|
|
else:
|
|
retval = self._trimEnd("%f" % newcheck)
|
|
|
|
if max_places is not None:
|
|
(whole, point, fraction) = retval.partition(".")
|
|
if point and len(fraction) > max_places:
|
|
if max_places == 0:
|
|
retval = whole
|
|
else:
|
|
retval = "%s.%s" % (whole, fraction[:max_places])
|
|
|
|
if abbr:
|
|
return retval + " " + abbr + _("B")
|
|
else:
|
|
return retval + " " + prefix + P_("byte", "bytes", newcheck)
|