diff --git a/forgetest/forgetest/catalog.py b/forgetest/forgetest/catalog.py index 801725e..b3fd77c 100644 --- a/forgetest/forgetest/catalog.py +++ b/forgetest/forgetest/catalog.py @@ -246,9 +246,9 @@ def module_parts(path): def sibling_imports(path, seen=None): """The suite modules a module's shared text imports with `from .x - import ...`, as paths, transitively, in a stable order. A helper a - test calls from a sibling module is part of the judge, so its text - belongs in the test's fingerprint.""" + import ...` or `from . import x`, as paths, transitively, in a stable + order. A helper a test calls from a sibling module is part of the + judge, so its text belongs in the test's fingerprint.""" if seen is None: seen = [] for sib in _direct_siblings(path): @@ -260,17 +260,19 @@ def sibling_imports(path, seen=None): def _imported(path, nodes): """The suite modules the statements `nodes` import with `from .x - import`, at any depth (inside a function or a block too), in the - order they are written.""" + import` or `from . import x` (each name a module of the suite), at any + depth (inside a function or a block too), in the order they are + written.""" here = os.path.dirname(path) found = sorted((n for top in nodes for n in ast.walk(top) - if isinstance(n, ast.ImportFrom) and n.level == 1 and n.module), + if isinstance(n, ast.ImportFrom) and n.level == 1), key=lambda n: (n.lineno, n.col_offset)) out = [] for node in found: - sib = os.path.join(here, node.module.replace(".", os.sep) + ".py") - if os.path.isfile(sib) and sib not in out: - out.append(sib) + for name in ([node.module] if node.module else [a.name for a in node.names]): + sib = os.path.join(here, name.replace(".", os.sep) + ".py") + if os.path.isfile(sib) and sib not in out: + out.append(sib) return out diff --git a/forgetest/tests/test_manifest.py b/forgetest/tests/test_manifest.py index c6b8d64..06da97c 100644 --- a/forgetest/tests/test_manifest.py +++ b/forgetest/tests/test_manifest.py @@ -536,8 +536,9 @@ class CatalogTests(unittest.TestCase): # a suite module does import, at its top level or inside a function # or a block, has to be on it (or be the registry, which judges # nothing), or that judge would be in no fingerprint. The sibling - # walk follows the relative spelling alone, so a suite module - # imports the package by no absolute name. + # walk follows the relative spellings alone (`from .x import` and + # `from . import x`), so a suite module imports the package by no + # absolute name. import ast import glob paths = catalog.shared_judge_paths() @@ -724,6 +725,15 @@ def two(ctx): a4, b4 = self.shas() self.assertNotEqual(a3, a4) self.assertNotEqual(b3, b4) + # the spelling with no module (`from . import x`, as homeoff.py imports cloud.py) is + # walked the same + self.write(self.MODULE.replace("def two(ctx):\n", + "def two(ctx):\n from . import judge as j\n j.judge(2)\n")) + a8, b8 = self.shas() + write_judge("def judge(x):\n return x > 4\n") + a9, b9 = self.shas() + self.assertEqual(a8, a9) + self.assertNotEqual(b8, b9) def test_line_endings_do_not_count(self): a1, b1 = self.shas()