[blivet:master+? 3/5] Rewrite _parseSpec() and convertTo()
mulhern
amulhern at redhat.com
Fri Oct 24 18:39:39 UTC 2014
This fixes a bug where most English specifiers would not be parsed
unless the locale was a lot like English; after failing to be parsed
in the English section, they still got their chance to be parsed in
the locale-specific section, and if it was close enough, as it usually
is for me, it would be parsed there.
Add a method, _parseUnits(), which handles the parsing of the units
part of the spec. Abstract this for both translated and non-translated.
If there is no exact match for the units, see if the units are a prefix
of a binary abbreviation.
Remove _makeSpecs() entirely, and redistribute its functionality between
new methods _parseUnits() and _makeSpec().
Rewrite convertTo() so that it uses newly introduced _parseUnits() not
newly removed _makeSpecs() and make it work with translated specs as well
as untranslated.
Thereby fix a small bug where the spec was lower-cased as if it were locale
specific, but the prefixes to match against were untranslated.
Change default to "", which means bytes, just like "b".
These changes emphasises that _parseSpec() and convertTo() should agree on
what a spec means.
Also, just accept the default for calls to convertTo() in size.
Remove now useless imports, constants, functions.
Update tests appropriately.
Signed-off-by: mulhern <amulhern at redhat.com>
---
blivet/size.py | 180 ++++++++++++++++++++++++++---------------------------
tests/size_test.py | 4 +-
2 files changed, 89 insertions(+), 95 deletions(-)
diff --git a/blivet/size.py b/blivet/size.py
index d0eecb6..70f01be 100644
--- a/blivet/size.py
+++ b/blivet/size.py
@@ -19,7 +19,6 @@
#
# Red Hat Author(s): David Cantrell <dcantrell at redhat.com>
-import itertools
import re
import string
import locale
@@ -41,6 +40,9 @@ _Prefix = namedtuple("Prefix", ["factor", "prefix", "abbr"])
_DECIMAL_FACTOR = 10 ** 3
_BINARY_FACTOR = 2 ** 10
+_BYTES_SYMBOL = N_(b"B")
+_BYTES_WORDS = [N_(b"bytes"), N_(b"byte")]
+
# Decimal prefixes for different size increments, along with the name
# and accepted abbreviation for the prefix. These prefixes are all
# for 'bytes'.
@@ -71,25 +73,6 @@ _BINARY_PREFIXES = [
# Empty prefix works both for decimal and binary
_EMPTY_PREFIX = _Prefix(1, b"", b"")
-_BYTES = [N_(b'B'), N_(b'b'), N_(b'byte'), N_(b'bytes')]
-_PREFIXES = _BINARY_PREFIXES + _DECIMAL_PREFIXES
-
-# Translated versions of the byte and prefix arrays as lazy comprehensions
-def _xlated_bytes():
- return (_(b) for b in _BYTES)
-
-def _xlated_prefix(p):
- return _Prefix(p.factor, _(p.prefix), _(p.abbr))
-
-def _xlated_binary_prefixes():
- return (_xlated_prefix(p) for p in _BINARY_PREFIXES)
-
-def _xlated_decimal_prefixes():
- return (_xlated_prefix(p) for p in _DECIMAL_PREFIXES)
-
-def _xlated_prefixes():
- return itertools.chain(_xlated_binary_prefixes(), _xlated_decimal_prefixes())
-
if six.PY2:
_ASCIIlower_table = string.maketrans(string.ascii_uppercase, string.ascii_lowercase) # pylint: disable=no-member
else:
@@ -107,30 +90,74 @@ def _lowerASCII(s):
else:
return str.translate(s, _ASCIIlower_table) # pylint: disable=no-member
-def _makeSpecs(prefix, abbr, xlate):
- """ Internal method used to generate a list of specifiers. """
- specs = []
+def _makeSpec(prefix, suffix, xlate, lowercase=True):
+ """ Synthesizes a whole word from prefix and suffix.
- if prefix:
- if xlate:
- specs.append(prefix.lower() + _(b"byte"))
- specs.append(prefix.lower() + _(b"bytes"))
- else:
- specs.append(_lowerASCII(prefix) + "byte")
- specs.append(_lowerASCII(prefix) + "bytes")
+ :param str prefix: a prefix
+ :param str suffixes: a suffix
+ :param bool xlate: if True, treat as locale specific
+ :param bool lowercase: if True, make all lowercase
- if abbr:
- if xlate:
- specs.append(abbr.lower() + _(b"b"))
- specs.append(abbr.lower())
- else:
- specs.append(_lowerASCII(abbr) + "b")
- specs.append(_lowerASCII(abbr))
+ :returns: whole word
+ :rtype: str
+ """
+ if xlate:
+ word = (_(prefix) + _(suffix))
+ return word.lower() if lowercase else word
+ else:
+ word = prefix + suffix
+ return _lowerASCII(word) if lowercase else word
+
+def _parseUnits(units, xlate):
+ """ Parse a unit specification and return corresponding factor.
+
+ :param units: a units specifier
+ :type units: any type of string like object
+ :param bool xlate: if True, assume locale specific
+
+ :returns: a numeric factor corresponding to the units, if found
+ :rtype: int or NoneType
+
+ Looks first for exact matches for a specifier, but, failing that,
+ searches for partial matches for abbreviations.
+
+ Normalizes units to lowercase, e.g., MiB and mib are treated the same.
+ """
+ if units == "":
+ return 1
+
+ if xlate:
+ units = units.lower()
+ else:
+ units = _lowerASCII(units)
+
+ # Search for complete matches
+ for factor, prefix, abbr in [_EMPTY_PREFIX] + _BINARY_PREFIXES + _DECIMAL_PREFIXES:
+ if units == _makeSpec(abbr, _BYTES_SYMBOL, xlate) or \
+ units in (_makeSpec(prefix, s, xlate) for s in _BYTES_WORDS):
+ return factor
- return specs
+ # Search for partial match of binary abbreviations
+ for factor, _prefix, abbr in _BINARY_PREFIXES:
+ if _makeSpec(abbr, "", xlate).startswith(units):
+ return factor
+
+ return None
def _parseSpec(spec):
- """ Parse string representation of size. """
+ """ Parse string representation of size.
+
+ :param spec: the specification of a size with, optionally, units
+ :type spec: any type of string like object
+ :returns: numeric value of the specification in bytes
+ :rtype: Decimal
+
+ :raises ValueError: if spec is unparseable
+
+ Tries to parse the spec first as English, if that fails, as
+ a locale specific string.
+ """
+
if not spec:
raise ValueError("invalid size specification", spec)
@@ -153,58 +180,32 @@ def _parseSpec(spec):
specifier = m.groups()[1]
- # Only attempt to parse as English if all characters are ASCII
+ # First try to parse as English.
try:
- # This will raise UnicodeDecodeError if specifier contains non-ascii
- # characters
if six.PY2:
- spec_ascii = specifier.decode("ascii")
- # Convert back to a str type for use with _lowerASCII
- spec_ascii = str(spec_ascii)
+ spec_ascii = str(specifier.decode("ascii"))
else:
- # This will raise UnicodeEncodeError if specifier contains any non-ascii
- # in Python3 `bytes` are new Python2 `str`
spec_ascii = bytes(specifier, 'ascii')
-
- # Use the ASCII-only lowercase mapping
- spec_ascii = _lowerASCII(spec_ascii)
except (UnicodeDecodeError, UnicodeEncodeError):
+ # String contains non-ascii characters, so can not be English.
pass
else:
- if spec_ascii and not spec_ascii.endswith("b"):
- spec_ascii += "ib"
-
- if spec_ascii in _BYTES or not spec_ascii:
- return size
-
- for factor, prefix, abbr in _PREFIXES:
- check = _makeSpecs(prefix, abbr, False)
-
- if spec_ascii in check:
- return size * factor
+ factor = _parseUnits(spec_ascii, False)
+ if factor is not None:
+ return size * factor
- # No English match found, try localized size specs. Convert the
- # specifier to a unicode string if it's not one already.
+ # No English match found, try localized size specs.
if six.PY2:
if isinstance(specifier, unicode):
spec_local = specifier
else:
spec_local = specifier.decode("utf-8")
else:
- # str = unicode in Python3
spec_local = specifier
- # Use the locale-specific lowercasing
- spec_local = spec_local.lower()
-
- if spec_local in _xlated_bytes():
- return size
-
- for factor, prefix, abbr in _xlated_prefixes():
- check = _makeSpecs(prefix, abbr, True)
-
- if spec_local in check:
- return size * factor
+ factor = _parseUnits(spec_local, True)
+ if factor is not None:
+ return size * factor
raise ValueError("invalid size specification", spec)
@@ -239,7 +240,7 @@ class Size(Decimal):
elif isinstance(value, (six.integer_types, float, Decimal)):
size = Decimal(value)
elif isinstance(value, Size):
- size = Decimal(value.convertTo("b"))
+ size = Decimal(value.convertTo())
else:
raise ValueError("invalid value %s for size" % value)
@@ -262,7 +263,7 @@ class Size(Decimal):
return "Size('%s')" % self
def __deepcopy__(self, memo):
- return Size(self.convertTo(spec="b"))
+ return Size(self.convertTo())
def __add__(self, other, context=None):
return Size(Decimal.__add__(self, other, context=context))
@@ -285,23 +286,16 @@ class Size(Decimal):
def __mod__(self, other, context=None):
return Size(Decimal.__mod__(self, other, context=context))
- def convertTo(self, spec="b"):
- """ Return the size in the units indicated by the specifier. The
- specifier can be prefixes from the _DECIMAL_PREFIXES and
- _BINARY_PREFIXES 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 Decimal(self)
+ def convertTo(self, spec="", xlate=False):
+ """ Return the size in the units indicated by the specifier.
- for factor, prefix, abbr in _PREFIXES:
- check = _makeSpecs(prefix, abbr, False)
-
- if spec in check:
- return Decimal(self) / Decimal(factor)
+ :param str spec: a units specifier
+ :returns: a numeric value in the units indicated by the specifier
+ :rtype: Decimal
+ """
+ divisor = _parseUnits(spec, xlate)
+ if divisor:
+ return Decimal(self) / Decimal(divisor)
return None
diff --git a/tests/size_test.py b/tests/size_test.py
index b577c70..a624aa8 100644
--- a/tests/size_test.py
+++ b/tests/size_test.py
@@ -28,7 +28,7 @@ from decimal import Decimal
import six
from blivet.errors import SizePlacesError
-from blivet.size import Size, _PREFIXES
+from blivet.size import Size, _EMPTY_PREFIX, _BINARY_PREFIXES, _DECIMAL_PREFIXES
if six.PY3:
long = int # pylint: disable=redefined-builtin
@@ -71,7 +71,7 @@ class SizeTestCase(unittest.TestCase):
numbytes = long(47)
self._prefixTestHelper(numbytes, 1, None, None)
- for factor, prefix, abbr in _PREFIXES:
+ for factor, prefix, abbr in [_EMPTY_PREFIX] + _BINARY_PREFIXES + _DECIMAL_PREFIXES:
self._prefixTestHelper(numbytes, factor, prefix, abbr)
def testHumanReadable(self):
--
1.9.3
More information about the anaconda-patches
mailing list