-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathnameutil.py
More file actions
168 lines (138 loc) · 6.41 KB
/
nameutil.py
File metadata and controls
168 lines (138 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# -*- coding: utf-8 -*-
"""Utilities for working with identifiers in macros.
Main purpose is to be able to query both direct and hygienically captured names
with a unified API.
"""
__all__ = ["isx", "getname",
"is_unexpanded_expr_macro", "is_unexpanded_block_macro"]
from ast import Name, Attribute, Subscript, Call, With
from mcpyrate.core import Done
from mcpyrate.quotes import is_captured_macro, is_captured_value, lookup_macro
# Here hygienic captures only come from `unpythonic.syntax` (unless there are
# also user-defined macros), and we use from-imports and bare names for anything
# `q[h[]]`'d; but any references that appear explicitly in the user code may use
# either bare `somename` or `unpythonic.somename`.
#
# TODO: How about `unpythonic.somemodule.somename`? Currently not detected.
#
# Note that in `mcpyrate`, a hygienic capture can contain the value of an
# arbitrary expression, which does not need to be bound to a name. In that
# case the "name" will be the unparsed source code of the expression. See
# the implementation of `mcpyrate.quotes.h`. That's harmless here since
# an expression won't produce an exact match on the name.
#
# Here we're mainly interested in the case where we have captured the value
# a name had at the use site of `h[]`, and even then, we just look at the name,
# not the actual value.
#
# TODO: Let's look at the value, not just the name. Requires changes to use sites,
# TODO: because currently `isx` doesn't know about the value the caller wants to
# TODO: check against.
#
# TODO: For our use cases, that value is usually a syntax transformer function
# TODO: defined somewhere in `unpythonic.syntax`, so we can use things like
# TODO: `q[h[letter]]` or `q[h[dof]]` in the let/do constructs to ensure that
# TODO: the workhorses resolve correctly at the use site, and still be able
# TODO: to detect the expanded forms of those constructs in the AST.
#
# TODO: The run-time value can be obtained at this end by
# TODO: `value = mcpyrate.quotes.lookup_value(key)`,
# TODO: provided that `key and (key[1] is not None)`.
# TODO: If the second element of the key is `None`, it means that
# TODO: program execution hasn't yet reached the point where the
# TODO: actual value capture triggers for that particular use of `h[]`.
def isx(tree, x, accept_attr=True):
"""Test whether tree is a reference to the name ``x`` (str).
Alternatively, ``x`` may be a predicate that accepts a ``str``
and returns whether it matches, to support more complex matching
(e.g. ``lambda name: name.startswith("foo")``).
Both bare names and attributes can be recognized, to support
both from-imports and regular imports of ``somemodule.x``.
We support:
- bare name ``x``
- the name ``x`` inside a `mcpyrate.core.Done`, which may be produced
by expanded `@namemacro`s
- the name ``x`` inside a `mcpyrate` hygienic capture, which may be
inserted during macro expansion
- ``x`` as an attribute (if ``accept_attr=True``)
"""
ismatch = x if callable(x) else lambda name: name == x
thename = getname(tree, accept_attr=accept_attr)
return thename is not None and ismatch(thename)
def getname(tree, accept_attr=True):
"""The cousin of ``isx``.
From the same types of trees, extract the name as str.
If no match on ``tree``, return ``None``.
"""
if isinstance(tree, Done):
return getname(tree.body, accept_attr=accept_attr)
if type(tree) is Name:
return tree.id
key = is_captured_value(tree) # AST -> (name, frozen_value) or False
if key: # TODO: Python 3.8+: use walrus assignment here
name, frozen_value = key
return name
if accept_attr and type(tree) is Attribute:
return tree.attr
return None
# TODO: This utility really wants to live in `mcpyrate`, as part of a macro destructuring subsystem.
# TODO: It needs to be made more general:
# - detect also macro invocations that have macro arguments
# - destructure macro arguments, if any
def is_unexpanded_expr_macro(macrofunction, expander, tree):
"""Check whether `tree` is an expr macro invocation bound to `macrofunction` in `expander`.
This accounts for hygienic macro captures and as-imports.
If there is a match, return the subscript slice, i.e. the tree that would be passed
to the macro function by the expander if the macro was expanded normally.
**CAUTION**: This function doesn't currently support detecting macros that
take macro arguments.
"""
if type(tree) is not Subscript:
return False
maybemacro = tree.value
# hygienic captures and as-imports
key = is_captured_macro(maybemacro)
if key: # TODO: Python 3.8+: use walrus assignment here
name_node = lookup_macro(key)
elif type(maybemacro) is Name:
name_node = maybemacro
else:
return False
# extract the expr
macro = expander.isbound(name_node.id)
if macro is macrofunction:
return tree.slice
return False
# TODO: This utility really wants to live in `mcpyrate`, as part of a macro destructuring subsystem.
# TODO: It needs to be made more general:
# - detect if there are several macros in the same `with`
# - destructure macro arguments, if any
# - destructure as-part, if any
def is_unexpanded_block_macro(macrofunction, expander, tree):
"""Check whether `tree` is an expr macro invocation bound to `macrofunction` in `expander`.
This accounts for hygienic macro captures and as-imports.
**CAUTION**: This function doesn't currently support several macros in the same `with`.
"""
if type(tree) is not With:
return False
ctxmanager = tree.items[0].context_expr
# optvars = tree.items[0].optional_vars # as-part
# body = tree.body
maybemacro = ctxmanager
# discard args if any
if type(maybemacro) is Subscript:
maybemacro = maybemacro.value
# Parenthesis syntax for macro arguments (deprecated; kept for backward compatibility)
elif type(maybemacro) is Call:
maybemacro = maybemacro.func
# hygienic captures and as-imports
key = is_captured_macro(maybemacro)
if key: # TODO: Python 3.8+: use walrus assignment here
name_node = lookup_macro(key)
elif type(maybemacro) is Name:
name_node = maybemacro
else:
return False
macro = expander.isbound(name_node.id)
return macro is macrofunction
# TODO: We might also need a utility to detect decorator macros.