Repository: Textualize/textual

Issue:
Allow for empty CSS variable definitions
Consider the following CSS snippets:

 1.
```sass
$x: 
* {
    background: red;
}
```

2.
```sass
$x:
* {
    background: $x;
}
```

3.
```sass
* {
    background: red;
}
$x:
```

All three should work with the following behaviour:
 - an empty variable definition is allowed; and
 - setting a style to an empty variable should unset that style.

For example, 2. should unset the background of all widgets.

As of now, 2. and 3. can't be parsed.
2. raises an error regarding an undefined variable and 3. raises an `AttributeError` inside `substitute_references` in `parse.py`.


Discussion:
(This issue is reserved for the [EuroPython 2023 sprint](https://ep2023.europython.eu/sprints). If you are not participating in the sprint, please refrain from working on this issue. Thanks!)

This isn't a trivial fix but it also isn't an impossible one.
If you like puzzles, this might be a good one for you!
Fixing this will likely require toying with the CSS parser in `src/textual/css/parser.py`.

Good luck and have fun!
We'll have a look (@eliasdorneles and I). We're sitting in RB116@EuroPython Sprints.
Sure thing. Feel free to join us at 103 if you want.
I've noticed that this also breaks, with the same error as 2 (undefined variable):

```
* {
    background: $x;
}

$x: red;
```

@rodrigogiraoserrao is this expected? do the CSS variables need to be declared before they're used?
I'm not sure if we want to have the ability of using a variable before defining it, but it doesn't shock me that you have to define it before using it.
So, I'd say to worry about the other cases first and then this could come after.

What do you think?

Resolving patch:
diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py
--- a/src/textual/css/parse.py
+++ b/src/textual/css/parse.py
@@ -269,6 +269,7 @@ def substitute_references(
             break
         if token.name == "variable_name":
             variable_name = token.value[1:-1]  # Trim the $ and the :, i.e. "$x:" -> "x"
+            variable_tokens = variables.setdefault(variable_name, [])
             yield token
 
             while True:
@@ -284,7 +285,7 @@ def substitute_references(
                 if not token:
                     break
                 elif token.name == "whitespace":
-                    variables.setdefault(variable_name, []).append(token)
+                    variable_tokens.append(token)
                     yield token
                 elif token.name == "variable_value_end":
                     yield token
@@ -293,7 +294,6 @@ def substitute_references(
                 elif token.name == "variable_ref":
                     ref_name = token.value[1:]
                     if ref_name in variables:
-                        variable_tokens = variables.setdefault(variable_name, [])
                         reference_tokens = variables[ref_name]
                         variable_tokens.extend(reference_tokens)
                         ref_location = token.location
@@ -307,7 +307,7 @@ def substitute_references(
                     else:
                         _unresolved(ref_name, variables.keys(), token)
                 else:
-                    variables.setdefault(variable_name, []).append(token)
+                    variable_tokens.append(token)
                     yield token
                 token = next(iter_tokens, None)
         elif token.name == "variable_ref":


Test patch:
diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py
--- a/tests/css/test_parse.py
+++ b/tests/css/test_parse.py
@@ -220,6 +220,22 @@ def test_undefined_variable(self):
         with pytest.raises(UnresolvedVariableError):
             list(substitute_references(tokenize(css, "")))
 
+    def test_empty_variable(self):
+        css = "$x:\n* { background:$x; }"
+        result = list(substitute_references(tokenize(css, "")))
+        assert [(t.name, t.value) for t in result] == [
+            ("variable_name", "$x:"),
+            ("variable_value_end", "\n"),
+            ("selector_start_universal", "*"),
+            ("whitespace", " "),
+            ("declaration_set_start", "{"),
+            ("whitespace", " "),
+            ("declaration_name", "background:"),
+            ("declaration_end", ";"),
+            ("whitespace", " "),
+            ("declaration_set_end", "}"),
+        ]
+
     def test_transitive_reference(self):
         css = "$x: 1\n$y: $x\n.thing { border: $y }"
         assert list(substitute_references(tokenize(css, ""))) == [