-
Notifications
You must be signed in to change notification settings - Fork 0
compass(tests): AST guards refuse the spellings they cannot read #266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8217139
0364f22
176cac3
a86d053
0f7e86e
051fc93
a96ca64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |||||||
|
|
||||||||
| import pytest | ||||||||
| import torch | ||||||||
| from test_runner_rpc_surface import _methods | ||||||||
| from torch.utils._python_dispatch import TorchDispatchMode | ||||||||
|
|
||||||||
| from atom.compass.runner import COMPASS_RUNNER_QUALNAME | ||||||||
|
|
@@ -64,13 +65,11 @@ | |||||||
|
|
||||||||
| def _classes(path): | ||||||||
| tree = ast.parse(path.read_text()) | ||||||||
| for n in ast.walk(tree): | ||||||||
| n.file = path.name | ||||||||
| return {n.name: n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)} | ||||||||
|
|
||||||||
|
|
||||||||
| def _methods(node): | ||||||||
| return {n.name for n in node.body if isinstance(n, ast.FunctionDef)} | ||||||||
|
|
||||||||
|
|
||||||||
| def _self_calls(node): | ||||||||
| """Names of `self.x(...)` calls anywhere inside a function definition.""" | ||||||||
| return { | ||||||||
|
|
@@ -207,25 +206,67 @@ def _method_def(node, name): | |||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| def _self_assigned(node): | ||||||||
| """Every `self.x = ...` in *node*, as (name, the source of its value). | ||||||||
| # Every name through which an attribute can be written without an assignment. | ||||||||
| _WRITERS = ("setattr", "__setattr__", "__dict__", "vars") | ||||||||
|
|
||||||||
|
|
||||||||
| def _on_self(node): | ||||||||
| return isinstance(node, ast.Attribute) and ast.unparse(node.value) == "self" | ||||||||
|
|
||||||||
|
|
||||||||
| def _self_assigned(node, unreadable=()): | ||||||||
| """Every binding of `self.x` in *node*, as (name, the source of its value). | ||||||||
|
|
||||||||
| Pairs, not a mapping keyed by name. A name can be assigned more than once -- | ||||||||
| `forward_vars` is bound to the dict of buffers and later rebound to a slot | ||||||||
| of the ring it already holds -- and a mapping keeps only the last binding | ||||||||
| walked, which here is the rebind. The rebind names no buffer, so keying by | ||||||||
| name dropped `forward_vars` out of the holder set entirely. Keeping the | ||||||||
| pairs is what lets a name count as a holder when *any* of its bindings is. | ||||||||
|
|
||||||||
| Read in every spelling that states the name: a target of `=`, including | ||||||||
| one inside a tuple or list, of an annotated or augmented `=`, and | ||||||||
| `setattr(self, "x", ...)`. Any other write -- a `for` or `with` target, and | ||||||||
| every other mention of `setattr`, `__setattr__`, `__dict__` or `vars`, | ||||||||
| however it is reached -- binds something this cannot read. Each of those | ||||||||
| must be listed in *unreadable* by its source text, or this refuses, so a | ||||||||
| spelling it cannot read fails here rather than leaving the set smaller | ||||||||
| than the class. | ||||||||
| """ | ||||||||
| return { | ||||||||
| (t.attr, ast.unparse(n.value)) | ||||||||
| bound, read = set(), set() | ||||||||
| for n in ast.walk(node): | ||||||||
| if isinstance(n, (ast.Assign, ast.AnnAssign, ast.AugAssign)): | ||||||||
| for target in n.targets if isinstance(n, ast.Assign) else [n.target]: | ||||||||
| for t in ast.walk(target): | ||||||||
| if _on_self(t) and isinstance(t.ctx, ast.Store) and n.value: | ||||||||
| bound.add((t.attr, ast.unparse(n.value))) | ||||||||
| read.add(id(t)) | ||||||||
| elif ( | ||||||||
| isinstance(n, ast.Call) | ||||||||
| and ast.unparse(n.func) == "setattr" | ||||||||
| and [ast.unparse(a) for a in n.args[:1]] == ["self"] | ||||||||
| and len(n.args) == 3 | ||||||||
| and isinstance(n.args[1], ast.Constant) | ||||||||
| and isinstance(n.args[1].value, str) | ||||||||
| ): | ||||||||
| bound.add((n.args[1].value, ast.unparse(n.args[2]))) | ||||||||
| read.add(id(n.func)) | ||||||||
| calls = {id(n.func): n for n in ast.walk(node) if isinstance(n, ast.Call)} | ||||||||
| unread = [ | ||||||||
| (n.lineno, ast.unparse(calls.get(id(n), n))) | ||||||||
| for n in ast.walk(node) | ||||||||
| if isinstance(n, ast.Assign) | ||||||||
| for t in n.targets | ||||||||
| if isinstance(t, ast.Attribute) | ||||||||
| and isinstance(t.value, ast.Name) | ||||||||
| and t.value.id == "self" | ||||||||
| } | ||||||||
| if id(n) not in read | ||||||||
| and ( | ||||||||
| (_on_self(n) and isinstance(n.ctx, ast.Store)) | ||||||||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. G1 (cycle 2). Blocking, principle 6. The
A generic "refuse every Store not on |
||||||||
| or getattr(n, "id", getattr(n, "attr", None)) in _WRITERS | ||||||||
| ) | ||||||||
| ] | ||||||||
| assert sorted(text for _, text in unread) == sorted(unreadable), ( | ||||||||
| f"{node.name} binds attributes on self in a form not read here: " | ||||||||
| + "; ".join(f"{node.file}:{line}: {text}" for line, text in sorted(unread)) | ||||||||
| + f" -- listed as expected: {sorted(unreadable)}" | ||||||||
| ) | ||||||||
| return bound | ||||||||
|
|
||||||||
|
|
||||||||
| def test_exactly_two_attributes_of_the_runner_hold_the_ring(): | ||||||||
|
|
@@ -244,7 +285,12 @@ def test_exactly_two_attributes_of_the_runner_hold_the_ring(): | |||||||
| buffer, one attribute deeper -- true, and outside a claim about attributes | ||||||||
| on the runner. | ||||||||
| """ | ||||||||
| assigned = _self_assigned(_classes(ATOM_RUNNER)["ModelRunner"]) | ||||||||
| # The three `setattr`s bind whatever the attention builders return -- | ||||||||
| # the KV cache and the per-request state, by names the source never states. | ||||||||
| assigned = _self_assigned( | ||||||||
| _classes(ATOM_RUNNER)["ModelRunner"], | ||||||||
| unreadable=["setattr(self, name, value)"] * 3, | ||||||||
| ) | ||||||||
| holders = {n for n, v in assigned if any(t in v for t in BUFFER_TERMS)} | ||||||||
| assert holders == {"forward_vars", "_fv_ring"} | ||||||||
|
|
||||||||
|
|
@@ -488,7 +534,17 @@ def test_only_the_binding_module_reaches_the_engine(path): | |||||||
| The exemption is exactly the two packages whose closure something asserts; | ||||||||
| widening it to `atom.compass` would exempt packages nothing has checked. | ||||||||
| """ | ||||||||
| imported = _import_time_imports(path.read_text()) | ||||||||
| # A relative import is resolved against this module's package first, so | ||||||||
| # one that climbs out of the package is read as the module it names. | ||||||||
| package = path.relative_to(REPO).parent.parts | ||||||||
| imported = { | ||||||||
| ( | ||||||||
| ".".join([*package[: len(package) + 1 - level], name[level:]]).strip(".") | ||||||||
| if (level := len(name) - len(name.lstrip("."))) | ||||||||
| else name | ||||||||
| ) | ||||||||
| for name in _import_time_imports(path.read_text()) | ||||||||
| } | ||||||||
| engine = {m for m in imported if m.split(".")[0] == "atom"} - { | ||||||||
| m | ||||||||
| for m in imported | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
G5 (cycle 2). Not blocking, principle 6 and principle 8 (on the docstring's claim). F1, F2 and F7 now refuse every identifier spelling of the writers and of the helper; N10
__import__("builtins").setattris red. A name looked up as data still passes:176cac3d4; __builtins__["setattr"](self, "kv_cache", None)(overrides.py:246)}; __builtins__["setattr"](seq, "kv_transfer_params" + "_output", …)(moriio_connector.py:997)def _all(): from atom.compass.runner import overrides as o; return getattr(o, "unanswered_rpc_names")(object)(step_output.py:60)Two cheap changes:
"setattr","__setattr__","__dict__","vars","unanswered_rpc_names"), as_writes_not_readalready does forBLOB_ATTR. That closes all three rows. None of the three scopes contains such a string today; measure that first."set" + "attr") is the same class as the dynamic imports this PR already records as out of reach. Record it there, and drop "however it is reached" from_self_assigned's docstring, because that overclaims.