Documentation

This commit is contained in:
jtm 2012-03-30 16:12:24 +00:00
parent b050925ba0
commit ba4fe2ea62
6 changed files with 356 additions and 197 deletions

@ -0,0 +1,4 @@
"""
Main lesscss parse library. Contains lexer and parser, along with
utility classes
"""

@ -1,9 +1,11 @@
# -*- coding: utf8 -*-
""" """
LESSCPY Color functions .. module:: lesscpy.lessc.color
:synopsis: Lesscpy Color functions
Copyright (c) Copyright (c)
See LICENSE for details. See LICENSE for details.
<jtm@robot.is> .. moduleauthor:: Jóhann T. Maríusson <jtm@robot.is>
""" """
import colorsys import colorsys
from . import utility from . import utility
@ -11,8 +13,10 @@ from . import utility
class Color(): class Color():
def process(self, expression): def process(self, expression):
""" Process color expression """ Process color expression
@param tuple: color expression args:
@return: string expression (tuple): color expression
returns:
str
""" """
a, o, b = expression a, o, b = expression
c1 = self._hextorgb(a) c1 = self._hextorgb(a)
@ -25,23 +29,29 @@ class Color():
r.append("%02x" % v) r.append("%02x" % v)
return ''.join(r) return ''.join(r)
def operate(self, a, b, o): def operate(self, left, right, operation):
""" Do operation on colors """ Do operation on colors
@param string: color args:
@param string: color left (str): left side
@param string: operator right (str): right side
operation (str): Operation
returns:
str
""" """
operation = { operation = {
'+': '__add__', '+': '__add__',
'-': '__sub__', '-': '__sub__',
'*': '__mul__', '*': '__mul__',
'/': '__truediv__' '/': '__truediv__'
}.get(o) }.get(operation)
v = getattr(a, operation)(b) return getattr(left, operation)(right)
return v
def rgb(self, *args): def rgb(self, *args):
""" """ Translate rgb(...) to color string
raises:
ValueError
returns:
str
""" """
if len(args) == 4: if len(args) == 4:
return self.rgba(*args) return self.rgba(*args)
@ -57,7 +67,11 @@ class Color():
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def rgba(self, *args): def rgba(self, *args):
""" """ Translate rgba(...) to color string
raises:
ValueError
returns:
str
""" """
if len(args) == 4: if len(args) == 4:
try: try:
@ -71,7 +85,11 @@ class Color():
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def hsl(self, *args): def hsl(self, *args):
""" """ Translate hsl(...) to color string
raises:
ValueError
returns:
str
""" """
if len(args) == 4: if len(args) == 4:
return self.hsla(*args) return self.hsla(*args)
@ -85,7 +103,11 @@ class Color():
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def hsla(self, *args): def hsla(self, *args):
""" """ Translate hsla(...) to color string
raises:
ValueError
returns:
str
""" """
if len(args) == 4: if len(args) == 4:
h, s, l, a = args h, s, l, a = args
@ -97,28 +119,46 @@ class Color():
return "rgba(%s,%s,%s,%s)" % tuple(color) return "rgba(%s,%s,%s,%s)" % tuple(color)
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def hue(self, *args): def hue(self, color, *args):
""" Return the hue value of a color
args:
color (str): color
raises:
ValueError
returns:
float
""" """
""" if color:
if args: h, l, s = self._hextohls(color)
h, l, s = self._hextohls(args[0]) return round(h * 360.0, 3)
return round(h * 360, 3)
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def saturation(self, *args): def saturation(self, color, *args):
""" Return the saturation value of a color
args:
color (str): color
raises:
ValueError
returns:
float
""" """
""" if color:
if args: h, l, s = self._hextohls(color)
h, l, s = self._hextohls(args[0]) return s * 100.0
return s * 100
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def lightness(self, *args): def lightness(self, color, *args):
""" Return the lightness value of a color
args:
color (str): color
raises:
ValueError
returns:
float
""" """
""" if color:
if args: h, l, s = self._hextohls(color)
h, l, s = self._hextohls(args[0]) return l * 100.0
return l * 100
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def opacity(self, *args): def opacity(self, *args):
@ -126,74 +166,97 @@ class Color():
""" """
pass pass
def lighten(self, *args): def lighten(self, color, diff, *args):
""" Lighten a color
args:
color (str): color
diff (str): percentage
returns:
str
""" """
""" if color and diff:
if len(args) == 2:
color, diff = args
return self._ophsl(color, diff, 1, '__add__') return self._ophsl(color, diff, 1, '__add__')
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def darken(self, *args): def darken(self, color, diff, *args):
""" Darken a color
args:
color (str): color
diff (str): percentage
returns:
str
""" """
""" if color and diff:
if len(args) == 2:
color, diff = args
return self._ophsl(color, diff, 1, '__sub__') return self._ophsl(color, diff, 1, '__sub__')
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def saturate(self, *args): def saturate(self, color, diff, *args):
""" Saturate a color
args:
color (str): color
diff (str): percentage
returns:
str
""" """
""" if color and diff:
if len(args) == 2:
color, diff = args
return self._ophsl(color, diff, 2, '__add__') return self._ophsl(color, diff, 2, '__add__')
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def desaturate(self, *args): def desaturate(self, color, diff, *args):
""" Desaturate a color
args:
color (str): color
diff (str): percentage
returns:
str
""" """
""" if color and diff:
if len(args) == 2:
color, diff = args
return self._ophsl(color, diff, 2, '__sub__') return self._ophsl(color, diff, 2, '__sub__')
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def clamp(self, v): def _clamp(self, value):
""" # Clamp value
""" return min(1, max(0, value))
return min(1, max(0, v))
def grayscale(self, *args): def grayscale(self, color, *args):
""" Simply 100% desaturate.
args:
color (str): color
returns:
str
""" """
Simply 100% desaturate. if color:
""" return self.desaturate(color, 100.0)
if len(args) == 2:
return self.desaturate(args[0], 100)
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def greyscale(self, *args): def greyscale(self, color, *args):
"""Wrapper for grayscale, other spelling
""" """
Wrapper for grayscale return self.grayscale(color, *args)
"""
return self.grayscale(*args)
def spin(self, *args): def spin(self, color, degree, *args):
""" Spin color by degree. (Increase / decrease hue)
args:
color (str): color
degree (str): percentage
raises:
ValueError
returns:
str
""" """
""" if color and degree:
if len(args) == 2: if type(degree) == str:
color, deg = args degree = int(degree.strip('%'))
if type(deg) == str: deg = int(deg.strip('%'))
h, l, s = self._hextohls(color) h, l, s = self._hextohls(color)
h = ((h * 360) + deg) % 360 h = ((h * 360.0) + degree) % 360.0
h = 360 + h if h < 0 else h h = 360.0 + h if h < 0 else h
rgb = colorsys.hls_to_rgb(h / 360, l, s) rgb = colorsys.hls_to_rgb(h / 360.0, l, s)
color = (round(c * 255) for c in rgb) color = (round(c * 255) for c in rgb)
return self._rgbatohex(color) return self._rgbatohex(color)
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def mix(self, *args): def mix(self, color1, color2, weight=50, *args):
""" """This algorithm factors in both the user-provided weight
This algorithm factors in both the user-provided weight
and the difference between the alpha values of the two colors and the difference between the alpha values of the two colors
to decide how to perform the weighted average of the two RGB values. to decide how to perform the weighted average of the two RGB values.
@ -216,19 +279,24 @@ class Color():
Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein
http://sass-lang.com http://sass-lang.com
args:
color1 (str): first color
color2 (str): second color
weight (int/str): weight
raises:
ValueError
returns:
str
""" """
if len(args) >= 2: if color1 and color2:
try: if type(weight) == str:
c1, c2, w = args weight = int(weight.strip('%'))
except ValueError: weight = ((weight / 100.0) * 2) - 1
c1, c2 = args rgb1 = self._hextorgb(color1)
w = 50 rgb2 = self._hextorgb(color2)
if type(w) == str: w = int(w.strip('%')) alpha = 0
w = ((w / 100.0) * 2) - 1 w1 = (((weight if weight * alpha == -1
rgb1 = self._hextorgb(c1) else weight + alpha) / (1 + weight * alpha)) + 1)
rgb2 = self._hextorgb(c2)
a = 0
w1 = (((w if w * a == -1 else w + a) / (1 + w * a)) + 1)
w1 = w1 / 2.0 w1 = w1 / 2.0
w2 = 1 - w1 w2 = 1 - w1
rgb = [ rgb = [
@ -240,10 +308,14 @@ class Color():
raise ValueError('Illegal color values') raise ValueError('Illegal color values')
def fmt(self, color): def fmt(self, color):
""" """ Format CSS Hex color code.
Format CSS Hex color code. uppercase becomes lowercase, 3 digit codes expand to 6 digit.
uppercase becomes lowercase, 3 digit codes expand to 6 digit. args:
@param string: color color (str): color
raises:
ValueError
returns:
str
""" """
if utility.is_color(color): if utility.is_color(color):
color = color.lower().strip('#') color = color.lower().strip('#')
@ -253,8 +325,6 @@ class Color():
raise ValueError('Cannot format non-color') raise ValueError('Cannot format non-color')
def _rgbatohex(self, rgba): def _rgbatohex(self, rgba):
"""
"""
return '#%s' % ''.join(["%02x" % v for v in return '#%s' % ''.join(["%02x" % v for v in
[0xff [0xff
if h > 0xff else if h > 0xff else
@ -262,8 +332,6 @@ class Color():
for h in rgba] for h in rgba]
]) ])
def _hextorgb(self, hex): def _hextorgb(self, hex):
"""
"""
hex = hex.strip() hex = hex.strip()
if hex[0] == '#': if hex[0] == '#':
hex = hex.strip('#').strip(';') hex = hex.strip('#').strip(';')
@ -275,17 +343,13 @@ class Color():
return [int(hex, 16)] * 3 return [int(hex, 16)] * 3
def _hextohls(self, hex): def _hextohls(self, hex):
"""
"""
rgb = self._hextorgb(hex) rgb = self._hextorgb(hex)
return colorsys.rgb_to_hls(*[c / 255.0 for c in rgb]) return colorsys.rgb_to_hls(*[c / 255.0 for c in rgb])
def _ophsl(self, color, diff, idx, op): def _ophsl(self, color, diff, idx, op):
"""
"""
if type(diff) == str: diff = int(diff.strip('%')) if type(diff) == str: diff = int(diff.strip('%'))
hls = list(self._hextohls(color)) hls = list(self._hextohls(color))
hls[idx] = self.clamp(getattr(hls[idx], op)(diff / 100)) hls[idx] = self._clamp(getattr(hls[idx], op)(diff / 100))
rgb = colorsys.hls_to_rgb(*hls) rgb = colorsys.hls_to_rgb(*hls)
color = (round(c * 255) for c in rgb) color = (round(c * 255) for c in rgb)
return self._rgbatohex(color) return self._rgbatohex(color)

@ -1,8 +1,8 @@
# -*- coding: utf8 -*- # -*- coding: utf8 -*-
""" """
.. module:: parser .. module:: lesscpy.lessc.parser
:synopsis: Lesscss parser. :synopsis: Lesscss parser.
http://www.dabeaz.com/ply/ply.html http://www.dabeaz.com/ply/ply.html
http://www.w3.org/TR/CSS21/grammar.html#scanner http://www.w3.org/TR/CSS21/grammar.html#scanner
http://lesscss.org/#docs http://lesscss.org/#docs
@ -33,7 +33,8 @@ class LessParser(object):
scope=None, scope=None,
outputdir='/tmp', outputdir='/tmp',
importlvl=0, importlvl=0,
verbose=False): verbose=False
):
""" Parser object """ Parser object
Kwargs: Kwargs:
@ -46,9 +47,9 @@ class LessParser(object):
importlvl (int): Import depth importlvl (int): Import depth
verbose (bool): Verbose mode verbose (bool): Verbose mode
""" """
self.verbose = verbose self.verbose = verbose
self.importlvl = importlvl self.importlvl = importlvl
self.lex = lexer.LessLexer() self.lex = lexer.LessLexer()
if not tabfile: if not tabfile:
tabfile = 'yacctab' tabfile = 'yacctab'

@ -1,16 +1,29 @@
""" """
.. module:: lesscpy.lessc.scope
:synopsis: Scope class.
Copyright (c)
See LICENSE for details.
.. moduleauthor:: Jóhann T. Maríusson <jtm@robot.is>
""" """
from . import utility from . import utility
class Scope(list): class Scope(list):
""" Scope class. A stack implementation.
"""
def __init__(self, init=False): def __init__(self, init=False):
"""Scope
Args:
init (bool): Initiate scope
"""
super().__init__() super().__init__()
self._mixins = {} self._mixins = {}
if init: self.push() if init: self.push()
self.in_mixin = False self.in_mixin = False
def push(self): def push(self):
""" """Push level on scope
""" """
self.append({ self.append({
'__variables__' : {}, '__variables__' : {},
@ -29,7 +42,9 @@ class Scope(list):
@property @property
def scopename(self): def scopename(self):
""" """Current scope name as list
Returns:
list
""" """
return [r['__current__'] return [r['__current__']
for r in self for r in self
@ -37,13 +52,17 @@ class Scope(list):
def add_block(self, block): def add_block(self, block):
""" """Add block element to scope
Args:
block (Block): Block object
""" """
self[-1]['__blocks__'].append(block) self[-1]['__blocks__'].append(block)
self[-1]['__names__'].append(block.raw()) self[-1]['__names__'].append(block.raw())
def add_mixin(self, mixin): def add_mixin(self, mixin):
""" """Add mixin to scope
Args:
mixin (Mixin): Mixin object
""" """
raw = mixin.name.raw() raw = mixin.name.raw()
if raw in self._mixins: if raw in self._mixins:
@ -52,13 +71,18 @@ class Scope(list):
self._mixins[raw] = [mixin] self._mixins[raw] = [mixin]
def add_variable(self, variable): def add_variable(self, variable):
""" """Add variable to scope
Args:
variable (Variable): Variable object
""" """
self[-1]['__variables__'][variable.name] = variable self[-1]['__variables__'][variable.name] = variable
def variables(self, name): def variables(self, name):
""" """Search for variable by name. Searches scope top down
Search for variable by name Args:
name (string): Search term
Returns:
Variable object OR False
""" """
if type(name) is tuple: if type(name) is tuple:
name = name[0] name = name[0]
@ -70,17 +94,19 @@ class Scope(list):
return False return False
def mixins(self, name): def mixins(self, name):
""" """ Search mixins for name.
Search mixins for name. Allow '>' to be ignored. '.a .b()' == '.a > .b()'
Allow '>' to be ignored. Args:
name (string): Search term
Returns:
Mixin object list OR False
""" """
m = self._smixins(name) m = self._smixins(name)
if m: return m if m: return m
return self._smixins(name.replace('?>?', ' ')) return self._smixins(name.replace('?>?', ' '))
def _smixins(self, name): def _smixins(self, name):
""" """Inner wrapper to search for mixins by name.
Inner wrapper to search for mixins by name.
""" """
return (self._mixins[name] return (self._mixins[name]
if name in self._mixins if name in self._mixins
@ -89,15 +115,18 @@ class Scope(list):
def blocks(self, name): def blocks(self, name):
""" """
Search for defined blocks recursively. Search for defined blocks recursively.
Allow '>' to be ignored. Allow '>' to be ignored. '.a .b' == '.a > .b'
Args:
name (string): Search term
Returns:
Block object OR False
""" """
b = self._blocks(name) b = self._blocks(name)
if b: return b if b: return b
return self._blocks(name.replace('?>?', ' ')) return self._blocks(name.replace('?>?', ' '))
def _blocks(self, name): def _blocks(self, name):
""" """Inner wrapper to search for blocks by name.
Inner wrapper to search for blocks by name.
""" """
i = len(self) i = len(self)
while i >= 0: while i >= 0:
@ -116,7 +145,11 @@ class Scope(list):
return False return False
def update(self, scope, at=0): def update(self, scope, at=0):
""" """Update scope. Add another scope to this one.
Args:
scope (Scope): Scope object
Kwargs:
at (int): Level to update
""" """
if hasattr(scope, '_mixins') and not at: if hasattr(scope, '_mixins') and not at:
self._mixins.update(scope._mixins) self._mixins.update(scope._mixins)
@ -125,13 +158,19 @@ class Scope(list):
self[at]['__names__'].extend(scope[at]['__names__']) self[at]['__names__'].extend(scope[at]['__names__'])
def swap(self, name): def swap(self, name):
""" """ Swap variable name for variable value
Args:
name (str): Variable name
Returns:
Variable value (Mixed)
""" """
if name.startswith('@@'): if name.startswith('@@'):
var = self.variables(name[1:]) var = self.variables(name[1:])
if var is False: raise SyntaxError('Unknown variable %s' % name) if var is False:
raise SyntaxError('Unknown variable %s' % name)
name = '@' + utility.destring(var.value[0]) name = '@' + utility.destring(var.value[0])
var = self.variables(name) var = self.variables(name)
if var is False: raise SyntaxError('Unknown variable %s' % name) if var is False:
raise SyntaxError('Unknown variable %s' % name)
return var.value return var.value

@ -1,41 +1,52 @@
# -*- coding: utf8 -*-
""" """
Utility functions .. module:: lesscpy.lessc.utility
:synopsis: various utility functions
Copyright (c) Copyright (c)
See LICENSE for details. See LICENSE for details.
<jtm@robot.is> .. moduleauthor:: Jóhann T. Maríusson <jtm@robot.is>
""" """
import collections import collections
import re import re
def flatten(ll): def flatten(lst):
"""Flatten list.
Args:
lst (list): List to flatten
Returns:
generator
""" """
Flatten list. for elm in lst:
@param ll: list if isinstance(elm, collections.Iterable) and not isinstance(elm, str):
@return: generator for sub in flatten(elm):
"""
for el in ll:
if isinstance(el, collections.Iterable) and not isinstance(el, str):
for sub in flatten(el):
yield sub yield sub
else: else:
yield el yield elm
def pairwise(lst): def pairwise(lst):
""" yield item i and item i+1 in lst. e.g. """ yield item i and item i+1 in lst. e.g.
(lst[0], lst[1]), (lst[1], lst[2]), ..., (lst[-1], None) (lst[0], lst[1]), (lst[1], lst[2]), ..., (lst[-1], None)
Args:
lst (list): List to process
Returns:
list
""" """
if not lst: return if not lst:
l = len(lst) return
for i in range(l-1): length = len(lst)
for i in range(length-1):
yield lst[i], lst[i+1] yield lst[i], lst[i+1]
yield lst[-1], None yield lst[-1], None
def rename(ll, scope): def rename(blocks, scope):
""" Rename all sub-blocks moved under another """ Rename all sub-blocks moved under another
block. (mixins) block. (mixins)
Args:
lst (list): block list
scope (object): Scope object
""" """
for p in ll: for p in blocks:
if hasattr(p, 'inner'): if hasattr(p, 'inner'):
p.name.parse(scope) p.name.parse(scope)
if p.inner: if p.inner:
@ -45,7 +56,11 @@ def rename(ll, scope):
scope.pop() scope.pop()
def blocksearch(block, name): def blocksearch(block, name):
""" Recursive search for name in block """ Recursive search for name in block (inner blocks)
Args:
name (str): search term
Returns:
Block OR False
""" """
for b in block.inner: for b in block.inner:
b = (b if b.raw() == name b = (b if b.raw() == name
@ -53,43 +68,59 @@ def blocksearch(block, name):
if b: return b if b: return b
return False return False
def reverse_guard(ll): def reverse_guard(lst):
""" """ Reverse guard expression. not
(@a > 5) -> (@a <= 5)
Args:
lst (list): Expression
returns:
list
""" """
rev = { rev = {
'<': '>', '<': '>=',
'>': '<', '>': '<=',
'=': '!=', '=': '!=',
'!=': '=', '!=': '=',
'>=': '<=', '>=': '<',
'<=': '>=' '<=': '>'
} }
return [rev[l] if l in rev else l for l in ll] return [rev[l] if l in rev else l for l in lst]
def debug_print(ll, lvl=0): def debug_print(lst, lvl=0):
""" """ Print scope tree
args:
lst (list): parse result
lvl (int): current nesting level
""" """
pad = ''.join(['\t.'] * lvl) pad = ''.join(['\t.'] * lvl)
t = type(ll) t = type(lst)
if t is list: if t is list:
for p in ll: for p in lst:
debug_print(p, lvl) debug_print(p, lvl)
elif hasattr(ll, 'tokens'): elif hasattr(lst, 'tokens'):
print(pad, t) print(pad, t)
debug_print(list(flatten(ll.tokens)), lvl+1) debug_print(list(flatten(lst.tokens)), lvl+1)
def destring(v): def destring(value):
""" Strip quotes """ Strip quotes from string
@param string: value args:
@return: string value (str)
returns:
str
""" """
return v.strip('"\'') return value.strip('"\'')
def analyze_number(var, err=''): def analyze_number(var, err=''):
""" Analyse number for type and split from unit """ Analyse number for type and split from unit
@param str: value 1px -> (q, 'px')
@raises: SyntaxError args:
@return: tuple (number, unit) var (str): number string
kwargs:
err (str): Error message
raises:
SyntaxError
returns:
tuple
""" """
n, u = split_unit(var) n, u = split_unit(var)
if type(var) is not str: if type(var) is not str:
@ -104,74 +135,94 @@ def analyze_number(var, err=''):
raise SyntaxError('%s ´%s´' % (err, var)) raise SyntaxError('%s ´%s´' % (err, var))
return (n, u) return (n, u)
def with_unit(n, u=None): def with_unit(number, unit=None):
""" Return number with unit """ Return number with unit
@param int/float: value args:
@param str: unit number (mixed): Number
@return: mixed unit (str): Unit
returns:
str
""" """
if type(n) is tuple: if type(number) is tuple:
n, u = n number, unit = number
if n == 0: return 0 if number == 0:
if u: return '0'
n = str(n) if unit:
if n.startswith('.'): number = str(number)
n = '0' + n if number.startswith('.'):
return "%s%s" % (n, u) number = '0' + number
return n return "%s%s" % (number, unit)
return number if type(number) is str else str(number)
def is_color(v): def is_color(value):
""" Is CSS color """ Is string CSS color
@param mixed: value args:
@return: bool value (str): string
returns:
bool
""" """
if not v or type(v) is not str: if not value or type(value) is not str:
return False return False
if v[0] == '#' and len(v) in [4, 5, 7, 9]: if value[0] == '#' and len(value) in [4, 5, 7, 9]:
try: try:
int(v[1:], 16) int(value[1:], 16)
return True return True
except Exception: except ValueError:
pass pass
return False return False
def is_variable(v): def is_variable(value):
""" Check if string is LESS variable """ Check if string is LESS variable
@param string: check args:
@return: bool value (str): string
returns:
bool
""" """
if type(v) is str: if type(value) is str:
return (v.startswith('@') or v.startswith('-@')) return (value.startswith('@') or value.startswith('-@'))
elif type(v) is tuple: elif type(value) is tuple:
v = ''.join(v) value = ''.join(value)
return (v.startswith('@') or v.startswith('-@')) return (value.startswith('@') or value.startswith('-@'))
return False return False
def is_int(v): def is_int(value):
""" Is value integer """ Is value integer
args:
value (str): string
returns:
bool
""" """
try: try:
int(str(v)) int(str(value))
return True return True
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
return False return False
def is_float(v): def is_float(value):
""" Is value float """ Is value float
args:
value (str): string
returns:
bool
""" """
if not is_int(v): if not is_int(value):
try: try:
float(str(v)) float(str(value))
return True return True
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
return False return False
def split_unit(v): def split_unit(value):
""" Split a number from its unit """ Split a number from its unit
1px -> (q, 'px')
Args:
value (str): input
returns:
tuple
""" """
r = re.search('^(\-?[\d\.]+)(.*)$', str(v)) r = re.search('^(\-?[\d\.]+)(.*)$', str(value))
return r.groups() if r else ('','') return r.groups() if r else ('','')

@ -86,9 +86,9 @@ class TestUtility(unittest.TestCase):
self.assertEqual('1.0px', test(1.0, 'px')) self.assertEqual('1.0px', test(1.0, 'px'))
self.assertEqual('0.0px', test('.0', 'px')) self.assertEqual('0.0px', test('.0', 'px'))
self.assertEqual('0.6px', test(.6, 'px')) self.assertEqual('0.6px', test(.6, 'px'))
self.assertEqual(1, test(1)) self.assertEqual('1', test(1))
self.assertEqual(1, test(1, None)) self.assertEqual('1', test(1, None))
self.assertEqual(1, test(1,)) self.assertEqual('1', test(1,))