Skip to content

Commit e4e2313

Browse files
committed
namedlambda: auto-name with source location info if no name candidate
1 parent ee655fc commit e4e2313

3 files changed

Lines changed: 35 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ The same applies if you need the macro parts of `unpythonic` (i.e. import anythi
7878

7979
- **Miscellaneous.**
8080
- `with namedlambda` now understands the walrus operator, too. In the construct `f := lambda ...: ...`, the lambda will get the name `f`. (Python 3.8 and later.)
81+
- `with namedlambda` now auto-names lambdas that don't have a name candidate using their source location info, if present. This makes it easy to see in a stack trace where some particular lambda was defined.
8182
- Add `unpythonic.dispatch.generic_addmethod`: add methods to a generic function defined elsewhere.
8283
- All documentation files now have a quick navigation section to skip to another part of the docs. (For all except the README, it's at the top.)
8384
- Python 3.8 and 3.9 support added.

unpythonic/syntax/lambdatools.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,13 @@ def iscurrywithfinallambda(tree):
263263
currycall_name = "currycall"
264264
iscurryf = lambda name: name in ("curryf", "curry") # auto or manual curry in a "with autocurry"
265265
def isautocurrywithfinallambda(tree):
266+
# "currycall(..., curryf(lambda ...: ...))"
266267
if not (type(tree) is Call and isx(tree.func, currycall_name) and tree.args and
267268
type(tree.args[-1]) is Call and isx(tree.args[-1].func, iscurryf)):
268269
return False
269-
return type(tree.args[-1].args[-1]) is Lambda
270+
curryf_callnode = tree.args[-1]
271+
lastarg = curryf_callnode.args[-1]
272+
return type(lastarg) is Lambda
270273

271274
def iscallwithnamedargs(tree):
272275
return type(tree) is Call and tree.keywords
@@ -279,11 +282,12 @@ def nameit(myname, tree):
279282
# The `has_deco` check ignores any already named lambdas.
280283
d = is_decorated_lambda(tree, mode="any") and not has_deco(["namelambda"], tree)
281284
c = iscurrywithfinallambda(tree)
282-
# this matches only during the second pass (after "with autocurry" has expanded)
285+
# This matches only during the second pass (after "with autocurry" has expanded)
283286
# so it can't have namelambda already applied
284287
if isautocurrywithfinallambda(tree): # "currycall(..., curryf(lambda ...: ...))"
285288
match = True
286289
thelambda = tree.args[-1].args[-1]
290+
# --> "currycall(..., (namelambda(myname))(curryf(lambda ...: ...)))"
287291
tree.args[-1].args[-1] = q[h[namelambda](u[myname])(a[thelambda])]
288292
elif type(tree) is Lambda or d or c:
289293
match = True
@@ -375,7 +379,30 @@ def transform(self, tree):
375379
newbody = dyn._macro_expander.visit(newbody)
376380

377381
# inside out: transform in expanded autocurry
378-
return NamedLambdaTransformer().visit(newbody)
382+
newbody = NamedLambdaTransformer().visit(newbody)
383+
384+
# v0.15.0+: Finally, auto-name any still anonymous `lambda` with source location info.
385+
# We must perform this in a separate pass so that expanded autocurry invocations
386+
# are transformed correctly first.
387+
class NamedLambdaFinalizationTransformer(ASTTransformer):
388+
def transform(self, tree):
389+
# Recurse into the lambda body in already named lambdas.
390+
if is_decorated_lambda(tree, mode="any") and has_deco(["namelambda"], tree):
391+
decorator_list, thelambda = destructure_decorated_lambda(tree)
392+
thelambda.body = self.visit(thelambda.body)
393+
return tree
394+
elif type(tree) is Lambda:
395+
if hasattr(tree, "lineno"):
396+
thename = f"<lambda at {dyn._macro_expander.filename}:{tree.lineno}>"
397+
tree, thelambda, match = nameit(thename, tree)
398+
if match:
399+
thelambda.body = self.visit(thelambda.body)
400+
else:
401+
tree = self.visit(tree)
402+
return tree
403+
return self.generic_visit(tree)
404+
return NamedLambdaFinalizationTransformer().visit(newbody)
405+
379406

380407
# The function `f` is adapted from the `f` macro in `macropy.quick_lambda`,
381408
# stripped into a bare syntax transformer., and then the `@Walker` inside

unpythonic/syntax/tests/test_lambdatools.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ def foo(func1, func2):
7070
func2=lambda x: x**2) # function call with named arg: name as "func2"
7171

7272
def bar(func1, func2):
73-
test[func1.__name__ == "<lambda>"]
74-
test[func2.__name__ == "<lambda>"]
75-
bar(lambda x: x**2, lambda x: x**2) # no naming when passed positionally
73+
test[func1.__name__.startswith("<lambda at")]
74+
test[func2.__name__.startswith("<lambda at")]
75+
bar(lambda x: x**2, lambda x: x**2) # name by source location info only when passed positionally
7676

7777
def baz(func1, func2):
78-
test[func1.__name__ == "<lambda>"]
78+
test[func1.__name__.startswith("<lambda at")]
7979
test[func2.__name__ == "func2"]
8080
baz(lambda x: x**2, func2=lambda x: x**2)
8181

0 commit comments

Comments
 (0)