Repository: spyder-ide/qtsass

Issue:
Unsupported qradialgradient
qradialgradient is not supported. Example of typical usage:

```
QRadioButton::indicator:checked
{
	background-color: qradialgradient(
		cx: 0.5, cy: 0.5,
		fx: 0.5, fy: 0.5,
		radius: 1.0,
		stop: 0.25 #78879b,
		stop: 0.3 #302F2F
	);
}
```

Comments:

Patch:
diff --git a/qtsass/api.py b/qtsass/api.py
index 7a65fe2..cdbfebf 100644
--- a/qtsass/api.py
+++ b/qtsass/api.py
@@ -22,7 +22,7 @@ import sass
 
 # Local imports
 from qtsass.conformers import qt_conform, scss_conform
-from qtsass.functions import qlineargradient, rgba
+from qtsass.functions import qlineargradient, qradialgradient, rgba
 from qtsass.importers import qss_importer
 
 
@@ -35,7 +35,11 @@ else:
 # yapf: enable
 
 # Constants
-DEFAULT_CUSTOM_FUNCTIONS = {'qlineargradient': qlineargradient, 'rgba': rgba}
+DEFAULT_CUSTOM_FUNCTIONS = {
+    'qlineargradient': qlineargradient,
+    'qradialgradient': qradialgradient,
+    'rgba': rgba
+}
 DEFAULT_SOURCE_COMMENTS = False
 
 # Logger setup
diff --git a/qtsass/conformers.py b/qtsass/conformers.py
index 2eaba07..d2df38f 100644
--- a/qtsass/conformers.py
+++ b/qtsass/conformers.py
@@ -18,8 +18,6 @@ import re
 
 # yapf: enable
 
-_DEFAULT_COORDS = ('x1', 'y1', 'x2', 'y2')
-
 
 class Conformer(object):
     """Base class for all text transformations."""
@@ -48,6 +46,8 @@ class NotConformer(Conformer):
 class QLinearGradientConformer(Conformer):
     """Conform QSS qlineargradient function."""
 
+    _DEFAULT_COORDS = ('x1', 'y1', 'x2', 'y2')
+
     qss_pattern = re.compile(
         r'qlineargradient\('
         r'((?:(?:\s+)?(?:x1|y1|x2|y2):(?:\s+)?[0-9A-Za-z$_\.-]+,?)+)'  # coords
@@ -68,8 +68,8 @@ class QLinearGradientConformer(Conformer):
             try:
                 key, value = key_values
                 key = key.strip()
-                if key in _DEFAULT_COORDS:
-                    pos = _DEFAULT_COORDS.index(key)
+                if key in self._DEFAULT_COORDS:
+                    pos = self._DEFAULT_COORDS.index(key)
                     if pos >= 0 and pos <= 3:
                         values[pos] = value.strip()
             except ValueError:
@@ -130,6 +130,116 @@ class QLinearGradientConformer(Conformer):
         return css
 
 
+class QRadialGradientConformer(Conformer):
+    """Conform QSS qradialgradient function."""
+
+    _DEFAULT_COORDS = ('cx', 'cy', 'radius', 'fx', 'fy')
+
+    qss_pattern = re.compile(
+        r'qradialgradient\('
+        # spread
+        r'((?:(?:\s+)?(?:spread):(?:\s+)?[0-9A-Za-z$_\.-]+,?)+)?'
+        # coords
+        r'((?:(?:\s+)?(?:cx|cy|radius|fx|fy):(?:\s+)?[0-9A-Za-z$_\.-]+,?)+)'
+        # stops
+        r'((?:(?:\s+)?stop:.*,?)+(?:\s+)?)?'
+        r'\)',
+        re.MULTILINE,
+    )
+
+    def _conform_spread_to_scss(self, group):
+        """
+        Take a qss str with xy coords and returns the values.
+
+          'spread: pad|repeat|reflect'
+        """
+        value = 'pad'
+        for key_values in [part.split(':', 1) for part in group.split(',')]:
+            try:
+                key, value = key_values
+                key = key.strip()
+                if key == 'spread':
+                    value = value.strip()
+            except ValueError:
+                pass
+        return value
+
+    def _conform_coords_to_scss(self, group):
+        """
+        Take a qss str with xy coords and returns the values.
+
+          'cx: 0, cy: 0, radius: 0, fx: 0, fy: 0' => '0, 0, 0, 0, 0'
+          'cy: 1' => '0, 1, 0, 0, 0'
+        """
+        values = ['0', '0', '0', '0', '0']
+        for key_values in [part.split(':', 1) for part in group.split(',')]:
+            try:
+                key, value = key_values
+                key = key.strip()
+                if key in self._DEFAULT_COORDS:
+                    pos = self._DEFAULT_COORDS.index(key)
+                    if pos >= 0:
+                        values[pos] = value.strip()
+            except ValueError:
+                pass
+        return ', '.join(values)
+
+    def _conform_stops_to_scss(self, group):
+        """
+        Take a qss str with stops and returns the values.
+
+          'stop: 0 red, stop: 1 blue' => '0 red, 1 blue'
+        """
+        new_group = []
+        split = [""]
+        bracket_level = 0
+        for char in group:
+            if not bracket_level and char == ",":
+                split.append("")
+                continue
+            elif char == "(":
+                bracket_level += 1
+            elif char == ")":
+                bracket_level -= 1
+            split[-1] += char
+
+        for part in split:
+            if part:
+                _, value = part.split(':', 1)
+                new_group.append(value.strip())
+        return ', '.join(new_group)
+
+    def to_scss(self, qss):
+        """
+        Conform qss qradialgradient to scss qradialgradient form.
+
+        Normalize all whitespace including the removal of newline chars.
+
+        qradialgradient(cx: 0, cy: 0, radius: 0,
+                        fx: 0, fy: 0, stop: 0 red, stop: 1 blue)
+        =>
+        qradialgradient(0, 0, 0, 0, 0, (0 red, 1 blue))
+        """
+        conformed = qss
+
+        for spread, coords, stops in self.qss_pattern.findall(qss):
+            new_spread = "'" + self._conform_spread_to_scss(spread) + "', "
+            conformed = conformed.replace(spread, new_spread, 1)
+            new_coords = self._conform_coords_to_scss(coords)
+            conformed = conformed.replace(coords, new_coords, 1)
+            if not stops:
+                continue
+
+            new_stops = ', ({})'.format(self._conform_stops_to_scss(stops))
+            conformed = conformed.replace(stops, new_stops, 1)
+
+        return conformed
+
+    def to_qss(self, css):
+        """Transform to qss from css."""
+        return css
+
+
 conformers = [c() for c in Conformer.__subclasses__() if c is not Conformer]
 
 
diff --git a/qtsass/functions.py b/qtsass/functions.py
index 89414cf..d4e0930 100644
--- a/qtsass/functions.py
+++ b/qtsass/functions.py
@@ -80,3 +80,30 @@ def qlineargradient(x1, y1, x2, y2, stops):
     template = 'qlineargradient(x1: {}, y1: {}, x2: {}, y2: {}, {})'
     return template.format(x1.value, y1.value, x2.value, y2.value,
                            ', '.join(stops_str))
+
+
+def qradialgradient(spread, cx, cy, radius, fx, fy, stops):
+    """
+    Implement qss qradialgradient function for scss.
+
+    :type spread: string
+    :type cx: sass.SassNumber
+    :type cy: sass.SassNumber
+    :type radius: sass.SassNumber
+    :type fx: sass.SassNumber
+    :type fy: sass.SassNumber
+    :type stops: sass.SassList
+    :return:
+    """
+    stops_str = []
+    for stop in stops[0]:
+        pos, color = stop[0]
+        stops_str.append('stop: {} {}'.format(
+            pos.value,
+            rgba_from_color(color),
+        ))
+    template = ('qradialgradient('
+                'spread: {}, cx: {}, cy: {}, radius: {}, fx: {}, fy: {}, {}'
+                ')')
+    return template.format(spread, cx.value, cy.value, radius.value, fx.value,
+                           fy.value, ', '.join(stops_str))


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index 14fb31f..181e658 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -43,6 +43,19 @@ QWidget {
     );
 }
 """
+QRADIANTGRADIENTS_STR = """
+QWidget {
+    background: qradialgradient(
+        spread: repeat,
+        cx: 0,
+        cy: 0,
+        fx: 0,
+        fy: 1,
+        stop: 0.1 blue,
+        stop: 0.8 green
+    );
+}
+"""
 QNOT_STR = """
 QLineEdit:!editable {
     background: white;
@@ -71,6 +84,7 @@ def test_compile_strings():
 
     qtsass.compile(COLORS_STR)
     qtsass.compile(QLINEARGRADIENTS_STR)
+    qtsass.compile(QRADIANTGRADIENTS_STR)
     qtsass.compile(QNOT_STR)
 
 
diff --git a/tests/test_conformers.py b/tests/test_conformers.py
index 1739036..64c01a3 100644
--- a/tests/test_conformers.py
+++ b/tests/test_conformers.py
@@ -15,7 +15,11 @@ from textwrap import dedent
 import unittest
 
 # Local imports
-from qtsass.conformers import NotConformer, QLinearGradientConformer
+from qtsass.conformers import (
+    NotConformer,
+    QLinearGradientConformer,
+    QRadialGradientConformer,
+)
 
 
 class TestNotConformer(unittest.TestCase):
@@ -155,5 +159,118 @@ class TestQLinearGradientConformer(unittest.TestCase):
                          self.css_float_coords_str)
 
 
+class TestQRadialGradientConformer(unittest.TestCase):
+
+    css_vars_str = "qradialgradient('$spread', $cx, $cy, $radius, $fx, $fy, (0 $red, 1 $blue))"
+    qss_vars_str = (
+        'qradialgradient(spread:$spread, cx:$cx, cy:$cy, radius:$radius, fx:$fx, fy:$fy,'
+        'stop: 0 $red, stop: 1 $blue)'
+    )
+
+    css_nostops_str = "qradialgradient('pad', 0, 0, 0, 0, 0)"
+    qss_nostops_str = 'qradialgradient(spread: pad, cx: 0, cy: 0, fx: 0, fy: 0)'
+
+    css_str = "qradialgradient('pad', 0, 0, 0, 0, 0, (0 red, 1 blue))"
+    qss_singleline_str = (
+        'qradialgradient(spread: pad, cx: 0, cy: 0, fx: 0, fy: 0, '
+        'stop: 0 red, stop: 1 blue)'
+    )
+    qss_multiline_str = dedent("""
+    qradialgradient(
+        spread: pad,
+        cx: 0,
+        cy: 0,
+        fx: 0,
+        fy: 0,
+        stop: 0 red,
+        stop: 1 blue
+    )
+    """).strip()
+    qss_weird_whitespace_str = (
+        'qradialgradient( spread: pad, cx: 0, cy:0, fx: 0, fy:0, '
+        '   stop:0 red, stop: 1 blue )'
+    )
+
+    css_rgba_str = (
+        "qradialgradient('pad', 0, 0, 0, 0, 0, "
+        "(0 rgba(0, 1, 2, 30%), 0.99 rgba(7, 8, 9, 100%)))"
+    )
+    qss_rgba_str = (
+        'qradialgradient(spread: pad, cx: 0, cy: 0, fx: 0, fy: 0, '
+        'stop: 0 rgba(0, 1, 2, 30%), stop: 0.99 rgba(7, 8, 9, 100%))'
+    )
+
+    css_incomplete_coords_str = (
+        "qradialgradient('pad', 0, 1, 0, 0, 0, (0 red, 1 blue))"
+    )
+
+    qss_incomplete_coords_str = (
+        'qradialgradient(spread:pad, cy:1, stop:0 red, stop: 1 blue)'
+    )
+
+    css_float_coords_str = (
+        "qradialgradient('pad', 0, 0.75, 0, 0, 0, (0 green, 1 pink))"
+    )
+
+    qss_float_coords_str = (
+        'qradialgradient(spread: pad, cy:0.75, stop:0 green, stop: 1 pink)'
+    )
+
+    def test_does_not_affect_css_form(self):
+        """QRadialGradientConformer no affect on css qradialgradient func."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.css_str), self.css_str)
+        self.assertEqual(c.to_qss(self.css_str), self.css_str)
+
+    def test_conform_singleline_str(self):
+        """QRadialGradientConformer singleline qss to scss."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_singleline_str), self.css_str)
+
+    def test_conform_multiline_str(self):
+        """QRadialGradientConformer multiline qss to scss."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_multiline_str), self.css_str)
+
+    def test_conform_weird_whitespace_str(self):
+        """QRadialGradientConformer weird whitespace qss to scss."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_weird_whitespace_str), self.css_str)
+
+    def test_conform_nostops_str(self):
+        """QRadialGradientConformer qss with no stops to scss."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_nostops_str), self.css_nostops_str)
+
+    def test_conform_vars_str(self):
+        """QRadialGradientConformer qss with vars to scss."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_vars_str), self.css_vars_str)
+
+    def test_conform_rgba_str(self):
+        """QRadialGradientConformer qss with rgba to scss."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_rgba_str), self.css_rgba_str)
+
+    def test_incomplete_coords(self):
+        """QRadialGradientConformer qss with not all 4 coordinates given."""
+
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_incomplete_coords_str),
+                         self.css_incomplete_coords_str)
+
+    def test_float_coords(self):
+        c = QRadialGradientConformer()
+        self.assertEqual(c.to_scss(self.qss_float_coords_str),
+                         self.css_float_coords_str)
+
+
 if __name__ == "__main__":
     unittest.main(verbosity=2)
diff --git a/tests/test_functions.py b/tests/test_functions.py
index bebbf8b..1821c3e 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -58,5 +58,21 @@ class TestQLinearGradientFunc(BaseCompileTest):
         )
 
 
+class TestQRadialGradientFunc(BaseCompileTest):
+    def test_color(self):
+        self.assertEqual(
+            self.compile_scss('qradialgradient(pad, 1, 2, 1, 3, 4, (0 red, 1 blue))'),
+            'qradialgradient(spread: pad, cx: 1.0, cy: 2.0, radius: 1.0, fx: 3.0, fy: 4.0, '
+            'stop: 0.0 rgba(255, 0, 0, 100%), stop: 1.0 rgba(0, 0, 255, 100%))'
+        )
+
+    def test_rgba(self):
+        self.assertEqual(
+            self.compile_scss('qradialgradient(pad, 1, 2, 1, 3, 4, (0 red, 0.2 rgba(5, 6, 7, 0.8)))'),
+            'qradialgradient(spread: pad, cx: 1.0, cy: 2.0, radius: 1.0, fx: 3.0, fy: 4.0, '
+            'stop: 0.0 rgba(255, 0, 0, 100%), stop: 0.2 rgba(5, 6, 7, 80%))'
+        )
+
+
 if __name__ == "__main__":
     unittest.main(verbosity=2)