Repository: classickit (difficulty: medium)

Issue:
topo_sort returns its result exactly backwards. Observed: topo_sort({'a': ['b'], 'b': []}) returns ['b', 'a'], but the edge a -> b requires a to appear before b, so the only valid answer for that graph is ['a', 'b']. On every DAG I tried, reversing the returned list yields a valid topological order, so the traversal itself looks right and only the final orientation is wrong. Cycle detection is unaffected: a self-loop, a two-node cycle, and a longer cycle inside a bigger graph all raise ValueError as documented. The empty graph still returns [] and an isolated node comes back alone. Note that a DAG generally admits many valid orders; the requirement is only that every returned list is a permutation of the nodes in which every edge (u, v) has u before v.

Source files:
core.py: """Topological ordering of a directed graph.

topo_sort(graph) takes a dict mapping every node (a string) to the list
of its direct successors; every node of the graph appears as a key. It
returns a list containing each node exactly once, ordered so that for
every edge (u, v), u appears BEFORE v. Many orders can be valid and any
one of them is acceptable; callers must only rely on the edge-before
property. If the graph contains a cycle (including a self-loop),
ValueError is raised. The empty graph yields [].
"""


def topo_sort(graph):
    """Return the nodes of graph in a topological order."""
    WHITE, GRAY, BLACK = 0, 1, 2
    color = {node: WHITE for node in graph}
    order = []

    def visit(node):
        color[node] = GRAY
        for succ in graph[node]:
            if color[succ] == GRAY:
                raise ValueError("graph contains a cycle through " + repr(succ))
            if color[succ] == WHITE:
                visit(succ)
        color[node] = BLACK
        order.append(node)

    for node in graph:
        if color[node] == WHITE:
            visit(node)
    return order


Test files:
test_core.py: import random

import pytest

from core import topo_sort


def assert_valid_order(graph, order):
    """order must be a permutation of the nodes respecting every edge."""
    assert sorted(order) == sorted(graph)
    position = {node: index for index, node in enumerate(order)}
    for node, successors in graph.items():
        for succ in successors:
            assert position[node] < position[succ]


def test_pinned_dags():
    assert topo_sort({"a": ["b"], "b": []}) == ["a", "b"]
    diamond = {
        "top": ["left", "right"],
        "left": ["bottom"],
        "right": ["bottom"],
        "bottom": [],
    }
    assert_valid_order(diamond, topo_sort(diamond))
    build = {
        "make": ["build"],
        "compile": ["link"],
        "build": [],
        "link": ["build"],
        "src": ["compile", "make"],
    }
    assert_valid_order(build, topo_sort(build))


def test_random_dags_edge_order():
    rng = random.Random(20280)
    for _ in range(200):
        size = rng.randint(2, 12)
        labels = ["n%d" % i for i in range(size)]
        rng.shuffle(labels)
        successors = {label: [] for label in labels}
        for i in range(size):
            for j in range(i + 1, size):
                if rng.random() < 0.35:
                    successors[labels[i]].append(labels[j])
        keys = list(labels)
        rng.shuffle(keys)
        graph = {key: successors[key] for key in keys}
        assert_valid_order(graph, topo_sort(graph))


def test_cycles_empty_and_acyclic_control():
    with pytest.raises(ValueError):
        topo_sort({"a": ["a"]})
    with pytest.raises(ValueError):
        topo_sort({"a": ["b"], "b": ["a"]})
    with pytest.raises(ValueError):
        topo_sort({"a": ["b"], "b": ["c"], "c": ["a"], "d": []})
    assert topo_sort({}) == []
    assert topo_sort({"solo": []}) == ["solo"]
    assert topo_sort({"u": ["v"], "v": []}) == ["u", "v"]


Patch:
--- a/core.py
+++ b/core.py
@@ -29,4 +29,5 @@
     for node in graph:
         if color[node] == WHITE:
             visit(node)
+    order.reverse()
     return order