# Copyright (c) 2008 - ALBAR (Toulouse, FRANCE). # mailto:barthe@albar.fr # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty 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., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Use epydoc (http://epydoc.sourceforge.net) to produce the documentation. """ This module manages I{sizes} of devices like block devices or system memory, as they are reported by various UNIX commands. It can be useful for people who often parse command outputs, log file, etc, looking for sizes in various units and formats to compare and compute them, then print results in a homogeneous way. Quick start =========== >>> from Size import mkSize >>> a = mkSize('20 Gb') # Default unit is 'm' >>> b = mkSize('0.1T', 'k') # Size in kilobytes from string in Tera >>> a, b (20480.0, 107374182.40000001) # a and b are float >>> a(), b() # sizes are callable ('20480 Mb', '107374182 Kb') # Results in human readable string >>> c = b - a # Sizes may be added or substracted >>> d = a * 3 # or divided or multiplied >>> d.g('%dMBytes') # Sizes may be converted in differents units '60GBytes' # and printed with differents formats >>> b.smartprint(6) # Size may be printed with the smallest unit '102 Gb' # that fills a given length >>> b.smartprint(10) # '104857 Mb' Description =========== This module defines the L{Size} class, subclass of the L{float} standard type. Object of this class are created through the L{mkSize} function. Size specification ------------------ A size is created from a string that represents a size with an optional unit (defaults to byte). The various forms of sizes expressed by various commands are recognized, and the recognising pattern is a compiled regular expression supported by the global variable L{RE_SIZE}, matched against B{the lowered} size specification. For example:: '20GB', '20.0 Gbytes', '20g', '20gB', '2000000000', '2000000000byte', '0' will match, while:: '20 gibi', '20 Go', '20 Gbyt' will not. Output format ------------- When a Size object is printed (i.e. is called, see below), the output follows a format. By default, it is the L{DEFAULT_FORMAT} constant (C{'%d Mb'}), which means that the size will be printed as an integer followed by a uppercase letter as unit and a 'b', such as C{20 Gb} or C{500 Kb}. A valid format must begin by a '%' followed. All caracters up to the first space or unit code is considered to be the format to print the number with. The first character of the rest is supposed to be a I{unit code} (see below) or an empty string. For example:: '%.02f Moctets', '%dGB', '%f' are valid formats, while:: '%d octets' is not. Unit codes ---------- Units are coded from a single lowercase character from B{b k m g t} for byte, kilobyte, megabyte, gigabyte and terabyte. Size object creation -------------------- The L{mkSize} function takes as argument a I{size specification} as a string and an optional unit code that defaults to B{'m'} (L{DEFAULT_UNIT} constant). If the I{unit} is I{None}, then the unit is the one defined in the I{size specification}. For example:: mkSize('20G') => unit defaults to 'm' (megabyte) mkSize('20G', None) => unit is 'g' mkSize('20G', 'k') => unit is in kilobytes Size object behaviour --------------------- A L{Size} object behaves as a L{float} (although using it in complex calculation is not a good idea), except that: - it cannot be negative nor less than one byte - it can be added or substracted to another Size object or a number, the result is a Size object. - it can be multiplied or divided by a number, the result is a Size object. - it can be compared to another Size object (regardless its unit) or a number When an object is compared or computed with a number, the operation is done on the object unit base. When two objects are computed, the resulting object has the same unit as the first object. Size object attributes and methods ---------------------------------- It has the attributes C{b}, C{k}, C{m}, C{g} and C{t}, that are other I{Size} objects in these respective units. >>> a = mkSize('20g') >>> a 20480.0 >>> a.k 20971520.0 >>> a.k() '20971520 Kb' Size objects are callable, the result is a string representation of the object, following a given format (by default the constant L{DEFAULT_FORMAT}, or as a parameter of the call). >>> a('%.02fGB') '20480.00MB' >>> a.g('%.02fGB') '20.00GB' Size objects can also be printed given a max length, choosing the unit that fills it, with the L{Size.smartprint} method. @version: 0.1 @status: beta @author: A. Barthe @copyright: 2008 - ALBAR (Toulouse, FRANCE) @contact: mailto:barthe@albar.fr """ import re, types ######################################################################## #{ Constants #: The multiplicator between two consecutive units. Could be 1000 BASE = 1024 #: The compiled regular expression that matches a B{lowered} I{size specification}. #: It must define 2 matching groups: #: - one for the value, #: - the other for the unit (maybe empty). The matched string must be #: one character that match a unit code (or 'b' by default) RE_SIZE = re.compile('([\d\.,]+)\s*([kmgt])?\s*b?(?:yte)?s?$') #: The default unit of I{Size} objects. Could be I{None} to preserve the original unit. DEFAULT_UNIT = 'm' #: The default format to be used to print I{Size} objects. DEFAULT_FORMAT = '%d Mb' #} #{ Factory function #----------------------------------------------------------------------- def mkSize(string, default_unit=DEFAULT_UNIT): """This function returns a Size object specified by the parameter I{string}. @param string: the size specification @param default_unit: unit of the Size object, as a I{unit code} or I{None}. @return: a L{Size} object. @raise ValueError: unmatched size specification """ if type(string) is not types.StringType: raise ValueError('Invalid size specification: %s' % string) string = string.lower() m = RE_SIZE.match(string) if m is None: raise ValueError('Invalid size specification: %s' % string) try: value = float(m.group(1).replace(',', '.')) except ValueError: raise ValueError('Invalid size specification: ' + string) unit = m.group(2) or 'b' if default_unit is not None: return _mksize(convert(value, unit, default_unit), default_unit) else: return _mksize(value, unit) #} #{ Low level factory function #----------------------------------------------------------------------- def _mksize(value, unit): """Returns a Size object from a numeric value and a unit. Should not be used directly. @param value: the numeric value @param unit: unit of the Size object, as a I{unit code} @return: a L{Size} object. @raise ValueError: negative or less than one byte value """ if value < 0: raise ValueError("Size must be a positive number or null: %s" % value) if value != 0 and convert(value, unit, 'b') < 1: raise ValueError("Size can't be less than one byte.") result = Size(value) result.unit = unit return result #} #{ Decorators #----------------------------------------------------------------------- def _num_method(otherisobj): "Decorator for computing functions. The flag says if the argument may be a I{Size} object or not." def decorator(f): def g(self, other): newother = otherisobj and self._getobj(other) or self._getnum(other) return _mksize(float.__dict__[f.__name__](self, newother), self.unit) return g return decorator #----------------------------------------------------------------------- def _cmp_method(f): "Decorator for comparison functions." def g(self, other): return float.__dict__[f.__name__](self, self._getobj(other)) return g #} ######################################################################## class Size(float): "Main class." #: Compiled regular expression to check output formats re_format = re.compile('(%[^bBkKmMgGtT\s]+)(.*)') #----------------------------------------------------------------------- def __getattr__(self, unit): """Returns an object with the same value and a different unit (or self if the requested unit if the unit of self) @raise AttributeError: the attribute name is not a I{unit code}.""" if len(unit) != 1 or unit not in 'bkmgt': raise AttributeError(unit) if unit == self.unit: return self new = _mksize(convert(self, self.unit, unit), unit) setattr(self, unit, new) return new #----------------------------------------------------------------------- def __call__(self, format=DEFAULT_FORMAT, unit=None): """Returns a string representation of the object, following a given I{format}, with a given I{unit}. Example: >>> a = mkSize('20 Gb') >>> a() '20480 Mb' >>> a('%.2f MB', 'g') '20.00 GB' >>> a.k('%dM') '20971520K' @param format: the output format, default to L{DEFAULT_FORMAT} @param unit: default is the unit of the object. @return: a string @raise ValueError: invalid format """ if unit is None: unit = self.unit m = self.re_format.match(format) if m is None: raise ValueError('%s: invalid format' % format) numfmt = m.group(1) unitfmt = m.group(2).lstrip() try: result = numfmt % getattr(self, unit) except TypeError: try: result = numfmt % int(getattr(self, unit)) except TypeError: raise ValueError('%s: invalid format' % format) result += ' ' * format.count(' ') if len(unitfmt) == 0: return result if unitfmt[0].isupper(): result += unit.upper() else: result += unit.lower() try: if format != DEFAULT_FORMAT or result[-1] not in 'bB': result += unitfmt[1:] except IndexError: pass return result #----------------------------------------------------------------------- def smartprint(self, length=8, format=DEFAULT_FORMAT, floor='b'): """Returns a string representation that fills a max length by choosing the unit. Example: >>> a = mkSize('20 Gb') >>> a.smartprint(3) '###' # too short >>> a.smartprint(4) '0 Tb' # not a good idea... >>> a.smartprint(5) '20 Gb' # better. >>> a.smartprint(15) '21474836480 B' # beurk. >>> a.smartprint(15, floor='m') '20480 Mb' # fine. @param length: the maximum length the rsult can be, defaults to 8 @param format: the output format, default to L{DEFAULT_FORMAT} @param floor: the lowest acceptable unit, defaults to the lowest one 'b' Must be a I{unit code}. @return: the expected string or a '#' padded string if the length is too short for the value. @raise ValueError: floor is not a I{unit code}. """ units = 'bkmgt' if len(floor) != 1 or floor not in units: raise ValueError('floor parameter must be in "%s": %s' % (units, floor)) for unit in units: if units.index(unit) < units.index(floor): continue result = self(format, unit) if len(result) <= length: return result return '#' * length #----------------------------------------------------------------------- def _getobj(self, value): """Returns a Size object from a value in various format. @param value: may be a string (supposed to be a I{size spec}), a numeric value or a Size object. @return: a new Size object B{with the same unit than self}. @raise ValueError: incorrect value """ if type(value) is Size: return getattr(value, self.unit) if type(value) is types.StringType: return mkSize(value, self.unit) if type(value) in (types.IntType, types.LongType, types.FloatType): result = _mksize(value, self.unit) return result raise ValueError('Incorrect value: %s' % value) #----------------------------------------------------------------------- def _getnum(self, value): """Returns I{value} if it is a positive number. @raise ValueError: incorrect value """ if not type(value) in (types.IntType, types.LongType, types.FloatType): raise ValueError("Must be a numeric type: %s" % value) if value < 0: raise ValueError("Must be a positive number: %s" % value) return value #----------------------------------------------------------------------- def __str__(self): return self() #----------------------------------------------------------------------- #{ Computing functions @_num_method(otherisobj=True) def __add__(self, other): pass @_num_method(otherisobj=True) def __sub__(self, other): pass @_num_method(otherisobj=False) def __mul__(self, other): pass @_num_method(otherisobj=False) def __div__(self, other): pass @_num_method(otherisobj=False) def __floordiv__(self, other): pass def __neg__(self): "Raises ValueError." raise ValueError("Size must be a positive number.") #} #----------------------------------------------------------------------- #{ Comparison functions @_cmp_method def __eq__(self, other): pass @_cmp_method def __ne__(self, other): pass @_cmp_method def __lt__(self, other): pass @_cmp_method def __le__(self, other): pass @_cmp_method def __gt__(self, other): pass @_cmp_method def __ge__(self, other): pass #} ######################################################################## ######################################################################## #{ Conversion funtions #----------------------------------------------------------------------- def x2m(value, unit): """Converts the I{value} from I{unit} to 'm'. @param value: a float @param unit: a unit code @return: the value converted from I{unit} to megabytes @raise ValueError: invalid unit code """ if unit == 'b': return value / BASE / BASE if unit == 'k': return value / BASE if unit == 'm': return value if unit == 'g': return value * BASE if unit == 't': return value * BASE * BASE raise ValueError('Invalid unit: %s' % unit) #----------------------------------------------------------------------- def m2x(value, unit): """Converts the I{value} from 'm' to I{unit}. @param value: a float @param unit: a unit code @return: the value converted from 'm' to I{unit} @raise ValueError: invalid unit code """ if unit == 'b': return value * BASE * BASE if unit == 'k': return value * BASE if unit == 'm': return value if unit == 'g': return value / BASE if unit == 't': return value / BASE / BASE raise ValueError('Invalid unit: %s' % unit) #----------------------------------------------------------------------- def convert(value, fromunit, tounit): """Converts the I{value} from I{fromunit} to I{tounit}. @param value: a float @param fromunit: a unit code @param tounit: a unit code @return: the value converted from I{fromunit} to I{tounit} @raise ValueError: invalid unit code """ return m2x(x2m(float(value), fromunit), tounit) #}