All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] data_smart: allow python snippets to include a dictionary
@ 2022-11-07 16:46 Mark Asselstine
  2022-11-10 14:42 ` [bitbake-devel] " Richard Purdie
  0 siblings, 1 reply; 2+ messages in thread
From: Mark Asselstine @ 2022-11-07 16:46 UTC (permalink / raw)
  To: bitbake-devel

[YOCTO #14917]

Attempting to use a dictionary in a python code snippet for variable
assignment results in an error. For example attempting something such
as

  IDX = "green"
  VAL = "${@{ 'green': 1, 'blue': 2 }[d.getVar('IDX')]}"

produces the error

  expansion of VAL threw ExpansionError: Failure expanding variable
  VAL, expression was ${@{ 'green': 1, 'blue': 2 }[d.getVar('IDX')]}
  which triggered exception SyntaxError: '{' was never closed (Var
  <VAL>, line 1)

The existing __expand_python_regexp__, "\${@.+?}", will match the
first close curly bracket encountered, resulting in incomplete and
un-parsable code, and thus produce the error. We can correct this by
allowing a single depth of nested curly brackets in
__expand_python_regexp__ by using "\${@(?:{.*?}|.)+?}", which will
match up to and including the matching close curly bracket to the
open, '${@', curly bracket, even if there are one or more singly
nested curly brackets present. This change allows the usecase
described above to function.

This change can't be made on its own though. The old regex would, in
an obscure way, handle the case where a python snippet contained an
unexpandable variable. Since the unexpandable variable is in curly
brackets it would cause incomplete/un-parsable python code and thus
remain unparsed. So something like

  VAL = "${@d.getVar('foo') + ${unsetvar}}"

would remain unparsed as the close curly bracket in "${unsetvar}"
would match and terminate the snippet prematurely. This quirk resulted
in the proper handling of python snippets with unexpanded
variables. With the change to __expand_python_regexp__ the full
snippet will match and be parsed, but to match the old/correct
behavior we would not want to parse it until ${unsetvar} can be
expanded. To ensure the old/correct behavior for python snippets with
unexpanded variables remains in place we add a check for unexpanded
variables in the python snippets before running them.

This handling of unparsed variables brings two benefits. The first we
now have an explicit check visible to all for unexpanded variables
instead of a somewhat hidden behavior. The second is that if there are
multiple python snippets the old behavior would run the code for each
but a single snippet with unexpanded variables would mean all snippets
would remain unparsed, meaning more and repeated processing at a later
time.

For example:
  "${@2*2},${@d.getVar('foo') ${unsetvar}}"
old behavior would give:
  "${@2*2},${@d.getVar('foo') ${unsetvar}}"
new behavior will give:
  "4,${@d.getVar('foo') ${unsetvar}}"

The old behavior would calculate '2*2' but toss the result when the
second snippet would fail to parse resulting in future recalculations
(or fetching from cache), while the new behavior avoids this.

Signed-off-by: Mark Asselstine <mark.asselstine@windriver.com>
---
 lib/bb/data_smart.py | 7 ++++++-
 lib/bb/tests/data.py | 9 +++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/lib/bb/data_smart.py b/lib/bb/data_smart.py
index dd20ca55..62d0c01c 100644
--- a/lib/bb/data_smart.py
+++ b/lib/bb/data_smart.py
@@ -29,7 +29,7 @@ logger = logging.getLogger("BitBake.Data")
 __setvar_keyword__ = [":append", ":prepend", ":remove"]
 __setvar_regexp__ = re.compile(r'(?P<base>.*?)(?P<keyword>:append|:prepend|:remove)(:(?P<add>[^A-Z]*))?$')
 __expand_var_regexp__ = re.compile(r"\${[a-zA-Z0-9\-_+./~:]+?}")
-__expand_python_regexp__ = re.compile(r"\${@.+?}")
+__expand_python_regexp__ = re.compile(r"\${@(?:{.*?}|.)+?}")
 __whitespace_split__ = re.compile(r'(\s)')
 __override_regexp__ = re.compile(r'[a-z0-9]+')
 
@@ -119,6 +119,11 @@ class VariableParse:
             else:
                 code = match.group()[3:-1]
 
+            # Do not run code that contains one or more unexpanded variables
+            # instead return the code with the characters we removed put back
+            if __expand_var_regexp__.findall(code):
+                return "${@" + code + "}"
+
             if self.varname:
                 varname = 'Var <%s>' % self.varname
             else:
diff --git a/lib/bb/tests/data.py b/lib/bb/tests/data.py
index e667c7c7..8c043b70 100644
--- a/lib/bb/tests/data.py
+++ b/lib/bb/tests/data.py
@@ -60,6 +60,15 @@ class DataExpansions(unittest.TestCase):
         val = self.d.expand("${@5*12}")
         self.assertEqual(str(val), "60")
 
+    def test_python_snippet_w_dict(self):
+        val = self.d.expand("${@{ 'green': 1, 'blue': 2 }['green']}")
+        self.assertEqual(str(val), "1")
+
+    def test_python_unexpanded_multi(self):
+        self.d.setVar("bar", "${unsetvar}")
+        val = self.d.expand("${@2*2},${foo},${@d.getVar('foo') + ' ${bar}'},${foo}")
+        self.assertEqual(str(val), "4,value_of_foo,${@d.getVar('foo') + ' ${unsetvar}'},value_of_foo")
+
     def test_expand_in_python_snippet(self):
         val = self.d.expand("${@'boo ' + '${foo}'}")
         self.assertEqual(str(val), "boo value_of_foo")
-- 
2.25.1



^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [bitbake-devel] [PATCH] data_smart: allow python snippets to include a dictionary
  2022-11-07 16:46 [PATCH] data_smart: allow python snippets to include a dictionary Mark Asselstine
@ 2022-11-10 14:42 ` Richard Purdie
  0 siblings, 0 replies; 2+ messages in thread
From: Richard Purdie @ 2022-11-10 14:42 UTC (permalink / raw)
  To: Mark Asselstine, bitbake-devel

On Mon, 2022-11-07 at 11:46 -0500, Mark Asselstine wrote:
> [YOCTO #14917]
> 
> Attempting to use a dictionary in a python code snippet for variable
> assignment results in an error. For example attempting something such
> as
> 
>   IDX = "green"
>   VAL = "${@{ 'green': 1, 'blue': 2 }[d.getVar('IDX')]}"
> 
> produces the error
> 
>   expansion of VAL threw ExpansionError: Failure expanding variable
>   VAL, expression was ${@{ 'green': 1, 'blue': 2 }[d.getVar('IDX')]}
>   which triggered exception SyntaxError: '{' was never closed (Var
>   <VAL>, line 1)
> 
> The existing __expand_python_regexp__, "\${@.+?}", will match the
> first close curly bracket encountered, resulting in incomplete and
> un-parsable code, and thus produce the error. We can correct this by
> allowing a single depth of nested curly brackets in
> __expand_python_regexp__ by using "\${@(?:{.*?}|.)+?}", which will
> match up to and including the matching close curly bracket to the
> open, '${@', curly bracket, even if there are one or more singly
> nested curly brackets present. This change allows the usecase
> described above to function.
> 
> This change can't be made on its own though. The old regex would, in
> an obscure way, handle the case where a python snippet contained an
> unexpandable variable. Since the unexpandable variable is in curly
> brackets it would cause incomplete/un-parsable python code and thus
> remain unparsed. So something like
> 
>   VAL = "${@d.getVar('foo') + ${unsetvar}}"
> 
> would remain unparsed as the close curly bracket in "${unsetvar}"
> would match and terminate the snippet prematurely. This quirk resulted
> in the proper handling of python snippets with unexpanded
> variables. With the change to __expand_python_regexp__ the full
> snippet will match and be parsed, but to match the old/correct
> behavior we would not want to parse it until ${unsetvar} can be
> expanded. To ensure the old/correct behavior for python snippets with
> unexpanded variables remains in place we add a check for unexpanded
> variables in the python snippets before running them.
> 
> This handling of unparsed variables brings two benefits. The first we
> now have an explicit check visible to all for unexpanded variables
> instead of a somewhat hidden behavior. The second is that if there are
> multiple python snippets the old behavior would run the code for each
> but a single snippet with unexpanded variables would mean all snippets
> would remain unparsed, meaning more and repeated processing at a later
> time.
> 
> For example:
>   "${@2*2},${@d.getVar('foo') ${unsetvar}}"
> old behavior would give:
>   "${@2*2},${@d.getVar('foo') ${unsetvar}}"
> new behavior will give:
>   "4,${@d.getVar('foo') ${unsetvar}}"
> 
> The old behavior would calculate '2*2' but toss the result when the
> second snippet would fail to parse resulting in future recalculations
> (or fetching from cache), while the new behavior avoids this.

Thanks for digging into and fixing this one, not a simple situation.
I'm particularly happy you added test cases.

I'm just hoping the cookie monster didn't frighten you away! :)

Cheers,

Richard


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2022-11-10 14:42 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-11-07 16:46 [PATCH] data_smart: allow python snippets to include a dictionary Mark Asselstine
2022-11-10 14:42 ` [bitbake-devel] " Richard Purdie

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.