From da896602199d2e645cca7bab88f7a49be11e7523 Mon Sep 17 00:00:00 2001 From: Max Willsey Date: Fri, 1 Dec 2017 13:56:35 -0800 Subject: [PATCH] Use @dataclass from attr package This makes it a lot easier to define simple classes. Plus, something like it is going to be standardized soon: https://www.python.org/dev/peps/pep-0557/ Note that an incompatibility between attr and mypy leads to typechecking errors when using @dataclass constructors because the __init__ function is generated. A plugin should be on the way: https://github.com/python/mypy/issues/2088 --- Pipfile | 1 + Pipfile.lock | 9 ++++++++- puddle/arch.py | 42 ++++++++++++++++------------------------- puddle/execution.py | 3 --- puddle/routing/astar.py | 40 +++++++++++++++++++-------------------- tests/test_execution.py | 4 ++-- 6 files changed, 46 insertions(+), 53 deletions(-) diff --git a/Pipfile b/Pipfile index cd26b0d..9961336 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ networkx = "*" typing = "*" PyYAML = "*" pigpio = "*" +attrs = "*" [dev-packages] mypy = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 5a7fb47..23ddf02 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "991847117722f4ef3621baa05ead85f75e03103ef36e91742deee68cbf9f0c3e" + "sha256": "a54248d2ed1e91363aca563c27ca812e8205f4d54a18df959f1c76fa325c86cc" }, "host-environment-markers": { "implementation_name": "cpython", @@ -29,6 +29,13 @@ ] }, "default": { + "attrs": { + "hashes": [ + "sha256:e7d51b70f19a4da5fe6b3c9938983e0af3b91e230edc504bd73c443d98037063", + "sha256:c78f53e32d7cf36d8597c8a2c7e3c0ad210f97b9509e152e4c37fa80869f823c" + ], + "version": "==17.3.0" + }, "click": { "hashes": [ "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", diff --git a/puddle/arch.py b/puddle/arch.py index e0e3901..d00c075 100644 --- a/puddle/arch.py +++ b/puddle/arch.py @@ -1,7 +1,8 @@ import networkx as nx import yaml -from typing import Tuple, Any, ClassVar, List, Dict +from attr import dataclass +from typing import Tuple, Any, ClassVar, List, Dict, Set from puddle.util import pairs @@ -9,22 +10,16 @@ log = logging.getLogger(__name__) -Node = Any +Location = Tuple[int, int] +# disable generation of cmp so it uses id-based hashing +@dataclass(cmp=False) class Droplet: - def __init__(self, info='a', cells=None): - self.info = info - self.locations: Set[Tuple] = cells or set() - self.valid = True - - def __str__(self): - invalid_str = '' if self.valid else 'INVALID, ' - return f'Droplet({invalid_str}{self.info!r})' - - def __repr__(self): - return f'{self} at 0x{id(self):x}' + info: Any + locations: Set[Location] + valid: bool = True def to_dict(self): """ Used to JSONify this for rendering in the client """ @@ -63,25 +58,21 @@ def mix(self, other: 'Droplet'): return Droplet(info, self.locations | other.locations) +@dataclass class Cell: - - def __init__(self, id: int, location: Tuple[Node, Node]) -> None: - self.id = id - self.location = location - - def __str__(self): - return f'{self.__class__.__name__}({self.location})' + pin: int + location: Location class Command: shape: ClassVar[nx.DiGraph] - input_locations: ClassVar[List[Node]] + input_locations: ClassVar[List[Location]] input_droplets: List[Droplet] result: Any strict: ClassVar[bool] = False - def run(self, mapping: Dict[Node, Node]): ... + def run(self, mapping: Dict[Location, Location]): ... class Move(Command): @@ -168,6 +159,7 @@ def run(self, mapping): class CollisionError(Exception): pass + class ArchitectureError(Exception): pass @@ -280,13 +272,11 @@ def from_string(cls, string, **kwargs): data = yaml.load(string) board = data['board'] - h = len(board) w = max(len(row) for row in board) empty_values = ['_', None] - # cells keyed by id cells = {} @@ -339,8 +329,8 @@ def from_file(cls, filename, **kwargs): arch.source_file = filename return arch - def spec_string(self, with_droplets=False): - """ Return the specification string of this Architecture. """ + def to_yaml_string(self, with_droplets=False): + """ Dump the Architecture to YAML string. """ lines = [ [' '] * self.width for _ in range(self.height) ] diff --git a/puddle/execution.py b/puddle/execution.py index 7adb5a1..8d0c1f1 100644 --- a/puddle/execution.py +++ b/puddle/execution.py @@ -9,9 +9,6 @@ import logging log = logging.getLogger(__name__) -# simple type aliases -Node = Any - class ExcecutionFailure(Exception): pass diff --git a/puddle/routing/astar.py b/puddle/routing/astar.py index 793d738..7b04e1f 100644 --- a/puddle/routing/astar.py +++ b/puddle/routing/astar.py @@ -2,8 +2,10 @@ import itertools import heapq -from typing import Dict, Set, Tuple, List, Any, Optional +from attr import dataclass, Factory +from typing import Dict, Tuple, List, Any, Optional +from puddle.arch import Location from puddle.util import manhattan_distance, neighborhood import networkx as nx @@ -12,36 +14,32 @@ log = logging.getLogger(__name__) -Node = Any -Path = List[Node] +Path = List[Location] class RouteFailure(Exception): pass +@dataclass(cmp=False) class Agent: + """ An agent to be routed. - def __init__(self, item, source, target, - collision_group: Optional[int] = None): - """ An agent to be routed. + collision_group of None can never collide with anything. + """ - collision_group of None can never collide with anything. - """ - - self.item = item - self.source = source - self.target = target - if collision_group is None: - # create a guaranteed unique group - self.collision_group = object() - else: - self.collision_group = collision_group + item: Any + source: Location + target: Location + # create a guaranteed unique group + collision_group: Optional[int] = Factory(object) class Router: - def __init__(self, graph: nx.DiGraph) -> None: + graph = nx.DiGraph + + def __init__(self, graph) -> None: self.graph = graph def route( @@ -80,7 +78,7 @@ def route( return paths @staticmethod - def build_path(predecessors: Dict[Node, Node], last) -> Path: + def build_path(predecessors: Dict[Location, Location], last) -> Path: """Reconstruct a path from the destination and a predecessor map.""" path = [] node = last @@ -113,10 +111,10 @@ def a_star(self, agent) -> Path: # Maps enqueued nodes to distance of discovered paths and the # computed heuristics to target. Saves recomputing heuristics. - enqueued: Dict[Node, Tuple[int, int]] = {} + enqueued: Dict[Location, Tuple[int, int]] = {} # Maps explored nodes to its predecessor on the shortest path. - explored: Dict[Node, Node] = {} + explored: Dict[Location, Location] = {} while todo: _, _, current, distance, time, parent = pop(todo) diff --git a/tests/test_execution.py b/tests/test_execution.py index 57308c3..e316ac9 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -8,8 +8,8 @@ # NOTE this does a little bit of badness by creating droplets without # locations. It works fine for now. @pytest.mark.parametrize('command_cls, droplets', [ - (Mix, [Droplet('a'), Droplet('b')]), - (Split, [Droplet('a')]), + (Mix, [Droplet('a', set()), Droplet('b', set())]), + (Split, [Droplet('a', set())]), ]) def test_place(arch, command_cls, droplets):