Skip to content

Commit ebb9464

Browse files
authored
Merge pull request #57 from amyreese/onnx
More robust evaluation of assignments
2 parents 2080bc9 + 10a837a commit ebb9464

File tree

2 files changed

+156
-24
lines changed

2 files changed

+156
-24
lines changed

dowsing/setuptools/setup_py_parsing.py

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
from typing import Any, Dict, Optional
99

1010
import libcst as cst
11-
from libcst.metadata import ParentNodeProvider, QualifiedNameProvider, ScopeProvider
11+
from libcst.metadata import (
12+
ParentNodeProvider,
13+
PositionProvider,
14+
QualifiedNameProvider,
15+
ScopeProvider,
16+
)
1217

1318
from ..types import Distribution
1419
from .setup_and_metadata import SETUP_ARGS
@@ -124,7 +129,12 @@ def leave_Call(
124129

125130

126131
class SetupCallAnalyzer(cst.CSTVisitor):
127-
METADATA_DEPENDENCIES = (ScopeProvider, ParentNodeProvider, QualifiedNameProvider)
132+
METADATA_DEPENDENCIES = (
133+
ScopeProvider,
134+
ParentNodeProvider,
135+
QualifiedNameProvider,
136+
PositionProvider,
137+
)
128138

129139
# TODO names resulting from other than 'from setuptools import setup'
130140
# TODO wrapper funcs that modify args
@@ -178,7 +188,9 @@ def visit_Call(self, node: cst.Call) -> Optional[bool]:
178188
BOOL_NAMES = {"True": True, "False": False, "None": None}
179189
PRETEND_ARGV = ["setup.py", "bdist_wheel"]
180190

181-
def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
191+
def evaluate_in_scope(
192+
self, item: cst.CSTNode, scope: Any, target_line: int = 0
193+
) -> Any:
182194
qnames = self.get_metadata(QualifiedNameProvider, item)
183195

184196
if isinstance(item, cst.SimpleString):
@@ -190,19 +202,36 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
190202
elif isinstance(item, cst.Name):
191203
name = item.value
192204
assignments = scope[name]
193-
for a in assignments:
194-
# TODO: Only assignments "before" this node matter if in the
195-
# same scope; really if we had a call graph and walked the other
196-
# way, we could have a better idea of what has already happened.
205+
assignment_nodes = sorted(
206+
(
207+
(self.get_metadata(PositionProvider, a.node).start.line, a.node)
208+
for a in assignments
209+
if a.node
210+
),
211+
reverse=True,
212+
)
213+
# Walk assignments from bottom to top, evaluating them recursively.
214+
for lineno, node in assignment_nodes:
215+
216+
# When recursing, only look at assignments above the "target line".
217+
if target_line and lineno >= target_line:
218+
continue
197219

198220
# Assign(
199221
# targets=[AssignTarget(target=Name(value="v"))],
200222
# value=SimpleString(value="'x'"),
201223
# )
224+
#
225+
# AugAssign(
226+
# target=Name(value="v"),
227+
# operator=AddAssign(...),
228+
# value=SimpleString(value="'x'"),
229+
# )
230+
#
202231
# TODO or an import...
203232
# TODO builtins have BuiltinAssignment
233+
204234
try:
205-
node = a.node
206235
if node:
207236
parent = self.get_metadata(ParentNodeProvider, node)
208237
if parent:
@@ -212,25 +241,37 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
212241
else:
213242
raise KeyError
214243
except (KeyError, AttributeError):
215-
return "??"
216-
217-
# This presumes a single assignment
218-
if not isinstance(gp, cst.Assign) or len(gp.targets) != 1:
219-
return "??" # TooComplicated(repr(gp))
244+
continue
220245

221246
try:
222247
scope = self.get_metadata(ScopeProvider, gp)
223248
except KeyError:
224249
# module scope isn't in the dict
225-
return "??"
250+
continue
251+
252+
# This presumes a single assignment
253+
if isinstance(gp, cst.Assign) and len(gp.targets) == 1:
254+
result = self.evaluate_in_scope(gp.value, scope, lineno)
255+
elif isinstance(parent, cst.AugAssign):
256+
result = self.evaluate_in_scope(parent, scope, lineno)
257+
else:
258+
# too complicated?
259+
continue
226260

227-
return self.evaluate_in_scope(gp.value, scope)
261+
# keep trying assignments until we get something other than ??
262+
if result != "??":
263+
return result
264+
265+
# give up
266+
return "??"
228267
elif isinstance(item, (cst.Tuple, cst.List)):
229268
lst = []
230269
for el in item.elements:
231270
lst.append(
232271
self.evaluate_in_scope(
233-
el.value, self.get_metadata(ScopeProvider, el)
272+
el.value,
273+
self.get_metadata(ScopeProvider, el),
274+
target_line,
234275
)
235276
)
236277
if isinstance(item, cst.Tuple):
@@ -248,10 +289,10 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
248289
for arg in item.args:
249290
if isinstance(arg.keyword, cst.Name):
250291
args[names.index(arg.keyword.value)] = self.evaluate_in_scope(
251-
arg.value, scope
292+
arg.value, scope, target_line
252293
)
253294
else:
254-
args[i] = self.evaluate_in_scope(arg.value, scope)
295+
args[i] = self.evaluate_in_scope(arg.value, scope, target_line)
255296
i += 1
256297

257298
# TODO clear ones that are still default
@@ -264,26 +305,30 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
264305
d = {}
265306
for arg in item.args:
266307
if isinstance(arg.keyword, cst.Name):
267-
d[arg.keyword.value] = self.evaluate_in_scope(arg.value, scope)
308+
d[arg.keyword.value] = self.evaluate_in_scope(
309+
arg.value, scope, target_line
310+
)
268311
# TODO something with **kwargs
269312
return d
270313
elif isinstance(item, cst.Dict):
271314
d = {}
272315
for el2 in item.elements:
273316
if isinstance(el2, cst.DictElement):
274317
d[self.evaluate_in_scope(el2.key, scope)] = self.evaluate_in_scope(
275-
el2.value, scope
318+
el2.value, scope, target_line
276319
)
277320
return d
278321
elif isinstance(item, cst.Subscript):
279-
lhs = self.evaluate_in_scope(item.value, scope)
322+
lhs = self.evaluate_in_scope(item.value, scope, target_line)
280323
if isinstance(lhs, str):
281324
# A "??" entry, propagate
282325
return "??"
283326

284327
# TODO: Figure out why this is Sequence
285328
if isinstance(item.slice[0].slice, cst.Index):
286-
rhs = self.evaluate_in_scope(item.slice[0].slice.value, scope)
329+
rhs = self.evaluate_in_scope(
330+
item.slice[0].slice.value, scope, target_line
331+
)
287332
try:
288333
if isinstance(lhs, dict):
289334
return lhs.get(rhs, "??")
@@ -296,15 +341,29 @@ def evaluate_in_scope(self, item: cst.CSTNode, scope: Any) -> Any:
296341
# LOG.warning(f"Omit2 {type(item.slice[0].slice)!r}")
297342
return "??"
298343
elif isinstance(item, cst.BinaryOperation):
299-
lhs = self.evaluate_in_scope(item.left, scope)
300-
rhs = self.evaluate_in_scope(item.right, scope)
344+
lhs = self.evaluate_in_scope(item.left, scope, target_line)
345+
rhs = self.evaluate_in_scope(item.right, scope, target_line)
346+
if lhs == "??" or rhs == "??":
347+
return "??"
301348
if isinstance(item.operator, cst.Add):
302349
try:
303350
return lhs + rhs
304351
except Exception:
305352
return "??"
306353
else:
307354
return "??"
355+
elif isinstance(item, cst.AugAssign):
356+
lhs = self.evaluate_in_scope(item.target, scope, target_line)
357+
rhs = self.evaluate_in_scope(item.value, scope, target_line)
358+
if lhs == "??" or rhs == "??":
359+
return "??"
360+
if isinstance(item.operator, cst.AddAssign):
361+
try:
362+
return lhs + rhs
363+
except Exception:
364+
return "??"
365+
else:
366+
return "??"
308367
else:
309368
# LOG.warning(f"Omit1 {type(item)!r}")
310369
return "??"

dowsing/tests/setuptools.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,76 @@ def test_add_items(self) -> None:
344344
self.assertEqual(d.name, "aaaa1111")
345345
self.assertEqual(d.packages, ["a", "b", "c"])
346346
self.assertEqual(d.classifiers, "??")
347+
348+
def test_self_reference_assignments(self) -> None:
349+
d = self._read(
350+
"""\
351+
from setuptools import setup
352+
353+
version = "base"
354+
name = "foo"
355+
name += "bar"
356+
version = version + ".suffix"
357+
358+
classifiers = [
359+
"123",
360+
"abc",
361+
]
362+
363+
if True:
364+
classifiers = classifiers + ["xyz"]
365+
366+
setup(
367+
name=name,
368+
version=version,
369+
classifiers=classifiers,
370+
)
371+
"""
372+
)
373+
self.assertEqual(d.name, "foobar")
374+
self.assertEqual(d.version, "base.suffix")
375+
self.assertSequenceEqual(d.classifiers, ["123", "abc", "xyz"])
376+
377+
def test_circular_references(self) -> None:
378+
d = self._read(
379+
"""\
380+
from setuptools import setup
381+
382+
name = "foo"
383+
384+
foo = bar
385+
bar = version
386+
version = foo
387+
388+
classifiers = classifiers
389+
390+
setup(
391+
name=name,
392+
version=version,
393+
)
394+
"""
395+
)
396+
self.assertEqual(d.name, "foo")
397+
self.assertEqual(d.version, "??")
398+
self.assertEqual(d.classifiers, ())
399+
400+
def test_redefines_builtin(self) -> None:
401+
d = self._read(
402+
"""\
403+
import setuptools
404+
with open("CREDITS.txt", "r", encoding="utf-8") as fp:
405+
credits = fp.read()
406+
407+
long_desc = "a" + credits + "b"
408+
name = "foo"
409+
410+
kwargs = dict(
411+
long_description = long_desc,
412+
name = name,
413+
)
414+
415+
setuptools.setup(**kwargs)
416+
"""
417+
)
418+
self.assertEqual(d.name, "foo")
419+
self.assertEqual(d.description, "??")

0 commit comments

Comments
 (0)