Skip to content

Commit

Permalink
unreachable mode in search path. If cannot be reached, then error out.
Browse files Browse the repository at this point in the history
  • Loading branch information
nmichlo committed Apr 24, 2024
1 parent c015a9f commit 2cefa39
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 17 deletions.
46 changes: 39 additions & 7 deletions pydependence/_core/modules_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,22 @@ def _assert_no_duplicate_paths(*gs) -> None:
)


class UnreachableModeEnum(str, Enum):
error = "error"
skip = "skip"
keep = "keep"


class UnreachableModuleError(RuntimeError):
pass


def _find_modules(
*,
search_paths: "Optional[Sequence[Path]]",
package_paths: "Optional[Sequence[Path]]",
tag: str,
reachable_only: bool = False,
unreachable_mode: UnreachableModeEnum,
) -> "nx.DiGraph":
"""
Construct a graph of all modules found in the search paths and package paths.
Expand Down Expand Up @@ -163,12 +174,17 @@ def _find_modules(
raise RuntimeError(f"[BUG] Empty module name found in graph: {g}")

# reverse traverse from each node to the root to figure out which nodes are reachable, then filter them out.
if reachable_only:
if unreachable_mode in (UnreachableModeEnum.skip, UnreachableModeEnum.error):
reverse = g.reverse()
for node in list(g.nodes):
root = node.split(".")[0]
if not nx.has_path(reverse, node, root):
g.remove_node(node)
if unreachable_mode == UnreachableModeEnum.error:
raise UnreachableModuleError(
f"Unreachable module found: {node} from root: {root}, module is probably not marked as a package or is missing an __init__.py file!"
)
else:
g.remove_node(node)

# * DiGraph [ import_path -> Node(module_info) ]
return g
Expand Down Expand Up @@ -224,25 +240,41 @@ def add_modules_from_raw_imports(
return self._merge_module_graph(graph=g)

def add_modules_from_search_path(
self, search_path: Path, tag: Optional[str] = None
self,
search_path: Path,
tag: Optional[str] = None,
unreachable_mode: UnreachableModeEnum = UnreachableModeEnum.error,
) -> "ModulesScope":
if tag is None:
tag = search_path.name
warnings.warn(
f"No tag provided for search path: {repr(search_path)}, using path name as tag: {repr(tag)}"
)
graph = _find_modules(search_paths=[search_path], package_paths=None, tag=tag)
graph = _find_modules(
search_paths=[search_path],
package_paths=None,
tag=tag,
unreachable_mode=unreachable_mode,
)
return self._merge_module_graph(graph=graph)

def add_modules_from_package_path(
self, package_path: Path, tag: Optional[str] = None
self,
package_path: Path,
tag: Optional[str] = None,
unreachable_mode: UnreachableModeEnum = UnreachableModeEnum.error,
) -> "ModulesScope":
if tag is None:
tag = package_path.parent.name
warnings.warn(
f"No tag provided for package path: {repr(package_path)}, using parent name as tag: {repr(tag)}"
)
graph = _find_modules(search_paths=None, package_paths=[package_path], tag=tag)
graph = _find_modules(
search_paths=None,
package_paths=[package_path],
tag=tag,
unreachable_mode=unreachable_mode,
)
return self._merge_module_graph(graph=graph)

# ~=~=~ MODULE INFO ~=~=~ #
Expand Down
68 changes: 58 additions & 10 deletions tests/test_module_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
from pydependence._core.modules_resolver import ScopeResolvedImports
from pydependence._core.modules_scope import (
_find_modules, DuplicateModuleNamesError,
DuplicateModulePathsError, DuplicateModulesError, ModulesScope,
DuplicateModulePathsError, DuplicateModulesError, ModulesScope, UnreachableModeEnum,
UnreachableModuleError,
)

# ========================================================================= #
Expand Down Expand Up @@ -232,6 +233,7 @@ def test_find_modules_search_path(module_info):
search_paths=[PKGS_ROOT],
package_paths=None,
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)
assert set(results.nodes) == (reachable | unreachable)
assert set(results.edges) == edges_reachable
Expand All @@ -241,17 +243,27 @@ def test_find_modules_search_path(module_info):
search_paths=[PKGS_ROOT],
package_paths=None,
tag="test",
reachable_only=True,
unreachable_mode=UnreachableModeEnum.skip,
)
assert set(results.nodes) == reachable
assert set(results.edges) == edges_reachable

# error if unreachable
with pytest.raises(UnreachableModuleError, match="Unreachable module found: A.a4.a4i from root: A"):
_find_modules(
search_paths=[PKGS_ROOT],
package_paths=None,
tag="test",
unreachable_mode=UnreachableModeEnum.error,
)

# load missing
with pytest.raises(FileNotFoundError):
_find_modules(
search_paths=[PKGS_ROOT / 'THIS_DOES_NOT_EXIST'],
package_paths=None,
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)

# load file
Expand All @@ -261,13 +273,15 @@ def test_find_modules_search_path(module_info):
search_paths=[PKG_AST_TEST],
package_paths=None,
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)

# load subdir
results = _find_modules(
search_paths=[PKG_A],
package_paths=None,
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)
assert set(results.nodes) == {'a1', 'a2', 'a3', 'a3.a3i', 'a4.a4i'}

Expand All @@ -277,6 +291,7 @@ def test_find_modules_search_path(module_info):
search_paths=[PKGS_ROOT, PKGS_ROOT],
package_paths=None,
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)


Expand All @@ -297,6 +312,7 @@ def test_find_modules_pkg_path():
search_paths=None,
package_paths=[PKG_A],
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)
assert set(results.nodes) == (reachable_a | unreachable_a)

Expand All @@ -305,22 +321,33 @@ def test_find_modules_pkg_path():
search_paths=None,
package_paths=[PKG_A],
tag="test",
reachable_only=True,
unreachable_mode=UnreachableModeEnum.skip,
)
assert set(results.nodes) == reachable_a

# error if unreachable
with pytest.raises(UnreachableModuleError, match="Unreachable module found: A.a4.a4i from root: A"):
_find_modules(
search_paths=None,
package_paths=[PKG_A],
tag="test",
unreachable_mode=UnreachableModeEnum.error,
)

# load all
results = _find_modules(
search_paths=None,
package_paths=[PKG_B],
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)
assert set(results.nodes) == {'B', 'B.b1', 'B.b2'}

results = _find_modules(
search_paths=None,
package_paths=[PKG_C],
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)
assert set(results.nodes) == {'C'}

Expand All @@ -330,6 +357,7 @@ def test_find_modules_pkg_path():
search_paths=None,
package_paths=[PKGS_ROOT / 'THIS_DOES_NOT_EXIST.py'],
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)

# load conflicting modules -- reference same files but different search paths
Expand All @@ -338,6 +366,7 @@ def test_find_modules_pkg_path():
search_paths=None,
package_paths=[PKG_A, PKG_A / 'a1.py'],
tag="test",
unreachable_mode=UnreachableModeEnum.keep,
)


Expand All @@ -354,17 +383,20 @@ def test_modules_scope():
modules_all = modules_a | modules_b | modules_c | {"t_ast_parser"}

scope = ModulesScope()
scope.add_modules_from_package_path(PKG_A)
scope.add_modules_from_package_path(PKG_A, unreachable_mode=UnreachableModeEnum.keep)
assert set(scope.iter_modules()) == modules_a
# this should not edit the original if it fails
with pytest.raises(DuplicateModulePathsError):
scope.add_modules_from_package_path(PKG_A / 'a1.py')
scope.add_modules_from_package_path(PKG_A / 'a1.py', unreachable_mode=UnreachableModeEnum.keep)
with pytest.raises(DuplicateModulePathsError):
scope.add_modules_from_package_path(PKG_A)
scope.add_modules_from_package_path(PKG_A, unreachable_mode=UnreachableModeEnum.keep)
assert set(scope.iter_modules()) == modules_a
# handle unreachable
with pytest.raises(UnreachableModuleError):
scope.add_modules_from_package_path(PKG_A)

scope = ModulesScope()
scope.add_modules_from_search_path(PKGS_ROOT)
scope.add_modules_from_search_path(PKGS_ROOT, unreachable_mode=UnreachableModeEnum.keep)
assert set(scope.iter_modules()) == modules_all

scope = ModulesScope()
Expand All @@ -375,12 +407,16 @@ def test_modules_scope():

# merge scopes & check subsets
scope_all = ModulesScope()
scope_all.add_modules_from_search_path(PKGS_ROOT)
scope_all.add_modules_from_search_path(PKGS_ROOT, unreachable_mode=UnreachableModeEnum.keep)
assert set(scope_all.iter_modules()) == modules_all
with pytest.raises(UnreachableModuleError):
scope_all.add_modules_from_search_path(PKGS_ROOT)

scope_a = ModulesScope()
scope_a.add_modules_from_package_path(PKG_A)
scope_a.add_modules_from_package_path(PKG_A, unreachable_mode=UnreachableModeEnum.keep)
assert set(scope_a.iter_modules()) == modules_a
with pytest.raises(UnreachableModuleError):
scope_a.add_modules_from_package_path(PKG_A)

scope_b = ModulesScope()
scope_b.add_modules_from_package_path(PKG_B)
Expand Down Expand Up @@ -445,7 +481,7 @@ def test_error_instance_of():

def test_resolve_scope():
scope_ast = ModulesScope()
scope_ast = scope_ast.add_modules_from_package_path(PKG_AST_TEST)
scope_ast.add_modules_from_package_path(PKG_AST_TEST)

resolved = ScopeResolvedImports.from_scope(scope=scope_ast)
assert resolved._get_imports_sources_counts() == {
Expand All @@ -468,6 +504,18 @@ def test_resolve_scope():
}


def test_resolve_across_scopes():
scope_all = ModulesScope()
scope_all.add_modules_from_package_path(package_path=PKG_A, unreachable_mode=UnreachableModeEnum.keep)
scope_all.add_modules_from_package_path(package_path=PKG_B)
scope_all.add_modules_from_package_path(package_path=PKG_C)

with pytest.raises(UnreachableModuleError):
scope_all.add_modules_from_package_path(package_path=PKG_A)

# subscope


# ========================================================================= #
# END #
# ========================================================================= #

0 comments on commit 2cefa39

Please sign in to comment.