Skip to content

Commit 6b76dc5

Browse files
authored
chore(di): add @key and @value to collection expressions (#12028)
We add the `@key` and `@value` special identifier to allow users to refer to keys and values from a dict-like object. ## Checklist - [ ] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [ ] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent 122caa6 commit 6b76dc5

File tree

2 files changed

+30
-15
lines changed

2 files changed

+30
-15
lines changed

ddtrace/debugging/_expressions.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from ddtrace.debugging._safety import safe_getitem
4747
from ddtrace.internal.compat import PYTHON_VERSION_INFO as PY
4848
from ddtrace.internal.logger import get_logger
49+
from ddtrace.internal.safety import _isinstance
4950

5051

5152
DDASTType = Union[Dict[str, Any], Dict[str, List[Any]], Any]
@@ -126,7 +127,7 @@ def _make_function(self, ast: DDASTType, args: Tuple[str, ...], name: str) -> Fu
126127
return FunctionType(abstract_code.to_code(), {}, name, (), None)
127128

128129
def _make_lambda(self, ast: DDASTType) -> Callable[[Any, Any], Any]:
129-
return self._make_function(ast, ("_dd_it", "_locals"), "<lambda>")
130+
return self._make_function(ast, ("_dd_it", "_dd_key", "_dd_value", "_locals"), "<lambda>")
130131

131132
def _compile_direct_predicate(self, ast: DDASTType) -> Optional[List[Instr]]:
132133
# direct_predicate => {"<direct_predicate_type>": <predicate>}
@@ -200,12 +201,12 @@ def _compile_arg_predicate(self, ast: DDASTType) -> Optional[List[Instr]]:
200201
if ca is None:
201202
raise ValueError("Invalid argument: %r" % a)
202203

203-
return self._call_function(
204-
lambda i, c, _locals: f(c(_, _locals) for _ in i),
205-
ca,
206-
[Instr("LOAD_CONST", fb)],
207-
[Instr("LOAD_FAST", "_locals")],
208-
)
204+
def coll_iter(it, cond, _locals):
205+
if _isinstance(it, dict):
206+
return f(cond(k, k, v, _locals) for k, v in it.items())
207+
return f(cond(e, None, None, _locals) for e in it)
208+
209+
return self._call_function(coll_iter, ca, [Instr("LOAD_CONST", fb)], [Instr("LOAD_FAST", "_locals")])
209210

210211
if _type in {"startsWith", "endsWith"}:
211212
a, b = args
@@ -245,8 +246,8 @@ def _compile_direct_operation(self, ast: DDASTType) -> Optional[List[Instr]]:
245246
if not isinstance(arg, str):
246247
return None
247248

248-
if arg == "@it":
249-
return [Instr("LOAD_FAST", "_dd_it")]
249+
if arg in {"@it", "@key", "@value"}:
250+
return [Instr("LOAD_FAST", f"_dd_{arg[1:]}")]
250251

251252
return self._call_function(
252253
get_local, [Instr("LOAD_FAST", "_locals")], [Instr("LOAD_CONST", self.__ref__(arg))]
@@ -297,12 +298,12 @@ def _compile_arg_operation(self, ast: DDASTType) -> Optional[List[Instr]]:
297298
if ca is None:
298299
raise ValueError("Invalid argument: %r" % a)
299300

300-
return self._call_function(
301-
lambda i, c, _locals: type(i)(_ for _ in i if c(_, _locals)),
302-
ca,
303-
[Instr("LOAD_CONST", fb)],
304-
[Instr("LOAD_FAST", "_locals")],
305-
)
301+
def coll_filter(it, cond, _locals):
302+
if _isinstance(it, dict):
303+
return type(it)({k: v for k, v in it.items() if cond(k, k, v, _locals)})
304+
return type(it)(e for e in it if cond(e, None, None, _locals))
305+
306+
return self._call_function(coll_filter, ca, [Instr("LOAD_CONST", fb)], [Instr("LOAD_FAST", "_locals")])
306307

307308
if _type == "getmember":
308309
v, attr = args

tests/debugging/test_expressions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def __getitem__(self, name):
7474
({"eq": [{"ref": "hits"}, None]}, {"hits": None}, True),
7575
({"substring": [{"ref": "payload"}, 4, 7]}, {"payload": "hello world"}, "hello world"[4:7]),
7676
({"any": [{"ref": "collection"}, {"isEmpty": {"ref": "@it"}}]}, {"collection": ["foo", "bar", ""]}, True),
77+
({"any": [{"ref": "coll"}, {"isEmpty": {"ref": "@value"}}]}, {"coll": {0: "foo", 1: "bar", 2: ""}}, True),
78+
({"any": [{"ref": "coll"}, {"isEmpty": {"ref": "@value"}}]}, {"coll": {0: "foo", 1: "bar", 2: "baz"}}, False),
79+
({"any": [{"ref": "coll"}, {"isEmpty": {"ref": "@key"}}]}, {"coll": {"foo": 0, "bar": 1, "": 2}}, True),
80+
({"any": [{"ref": "coll"}, {"isEmpty": {"ref": "@key"}}]}, {"coll": {"foo": 0, "bar": 1, "baz": 2}}, False),
7781
({"startsWith": [{"ref": "local_string"}, "hello"]}, {"local_string": "hello world!"}, True),
7882
({"startsWith": [{"ref": "local_string"}, "world"]}, {"local_string": "hello world!"}, False),
7983
(
@@ -91,6 +95,16 @@ def __getitem__(self, name):
9195
{"collection": {"foo", "bar", ""}},
9296
{"foo", "bar"},
9397
),
98+
(
99+
{"filter": [{"ref": "collection"}, {"not": {"isEmpty": {"ref": "@value"}}}]},
100+
{"collection": {1: "foo", 2: "bar", 3: ""}},
101+
{1: "foo", 2: "bar"},
102+
),
103+
(
104+
{"filter": [{"ref": "collection"}, {"not": {"isEmpty": {"ref": "@key"}}}]},
105+
{"collection": {"foo": 1, "bar": 2, "": 3}},
106+
{"foo": 1, "bar": 2},
107+
),
94108
({"contains": [{"ref": "payload"}, "hello"]}, {"payload": CustomObject("contains")}, SideEffect),
95109
(
96110
{"contains": [{"ref": "payload"}, "hello"]},

0 commit comments

Comments
 (0)