-
Notifications
You must be signed in to change notification settings - Fork 19
Add nominate_cycle_breakers method
#253
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
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| use crate::errors::GrimpResult; | ||
| use crate::graph::{Graph, ModuleToken}; | ||
| use rustc_hash::FxHashSet; | ||
|
|
||
| impl Graph { | ||
| pub fn nominate_cycle_breakers( | ||
| &self, | ||
| _package: ModuleToken, | ||
| ) -> GrimpResult<FxHashSet<(ModuleToken, ModuleToken)>> { | ||
| Ok(FxHashSet::default()) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| from grimp.application.graph import ImportGraph | ||
| import pytest | ||
|
|
||
|
|
||
| class TestNominateCycleBreakers: | ||
| def test_empty_graph(self): | ||
| graph = ImportGraph() | ||
| graph.add_module("pkg") | ||
|
|
||
| result = graph.nominate_cycle_breakers("pkg") | ||
|
|
||
| assert result == set() | ||
|
|
||
| @pytest.mark.parametrize( | ||
| "module", | ||
| ( | ||
| "pkg", | ||
| "pkg.foo", | ||
| "pkg.foo.blue", | ||
| ), | ||
| ) | ||
| def test_graph_with_no_imports(self, module: str): | ||
| graph = self._build_graph_with_no_imports() | ||
|
|
||
| result = graph.nominate_cycle_breakers(module) | ||
|
|
||
| assert result == set() | ||
|
|
||
| @pytest.mark.parametrize( | ||
| "module", | ||
| ( | ||
| "pkg", | ||
| "pkg.bar", | ||
| "pkg.foo.blue", | ||
| "pkg.foo.green", # Leaf package. | ||
| ), | ||
| ) | ||
| def test_acyclic_graph(self, module: str): | ||
| graph = self._build_acyclic_graph() | ||
|
|
||
| result = graph.nominate_cycle_breakers(module) | ||
|
|
||
| assert result == set() | ||
|
|
||
| def test_one_breaker(self): | ||
| graph = self._build_acyclic_graph() | ||
| importer, imported = "pkg.bar.red.four", "pkg.foo.blue.two" | ||
| graph.add_import(importer=importer, imported=imported) | ||
| result = graph.nominate_cycle_breakers("pkg") | ||
|
|
||
| assert result == {(importer, imported)} | ||
|
|
||
| def test_three_breakers(self): | ||
| graph = self._build_acyclic_graph() | ||
| imports = { | ||
| ("pkg.bar.red.four", "pkg.foo.blue.two"), | ||
| ("pkg.bar.yellow", "pkg.foo.blue.three"), | ||
| ("pkg.bar", "pkg.foo.blue.three"), | ||
| } | ||
| for importer, imported in imports: | ||
| graph.add_import(importer=importer, imported=imported) | ||
|
|
||
| result = graph.nominate_cycle_breakers("pkg") | ||
|
|
||
| assert result == imports | ||
|
|
||
| def test_nominated_based_on_dependencies_rather_than_imports(self): | ||
| graph = self._build_acyclic_graph() | ||
| # Add lots of imports from a single module - this will be treated as | ||
| # a single dependency. | ||
| importer, imported = "pkg.bar.red.four", "pkg.foo.blue.two" | ||
| for i in range(1, 30): | ||
| graph.add_import( | ||
| importer=importer, imported=imported, line_number=i, line_contents="-" | ||
| ) | ||
|
|
||
| graph.add_import(importer=importer, imported=imported) | ||
|
|
||
| result = graph.nominate_cycle_breakers("pkg") | ||
|
|
||
| assert result == {(importer, imported)} | ||
|
|
||
| def test_imports_between_passed_package_and_children_are_disregarded(self): | ||
| graph = self._build_acyclic_graph() | ||
| parent, child = "pkg.foo.blue", "pkg.foo" | ||
| graph.add_import(importer=parent, imported=child) | ||
| graph.add_import(importer=child, imported=parent) | ||
|
|
||
| result = graph.nominate_cycle_breakers(parent) | ||
|
|
||
| assert result == set() | ||
|
|
||
| def test_on_child_of_root(self): | ||
| graph = self._build_acyclic_graph() | ||
| imports = { | ||
| ("pkg.bar.red.five", "pkg.bar.yellow.eight"), | ||
| ("pkg.bar.red", "pkg.bar.yellow"), | ||
| } | ||
| for importer, imported in imports: | ||
| graph.add_import(importer=importer, imported=imported) | ||
|
|
||
| result = graph.nominate_cycle_breakers("pkg.bar") | ||
|
|
||
| assert result == imports | ||
|
|
||
| def test_on_grandchild_of_root(self): | ||
| graph = self._build_acyclic_graph() | ||
| imports = { | ||
| ("pkg.bar.orange.ten.gamma", "pkg.bar.orange.nine.alpha"), | ||
| ("pkg.bar.orange.ten", "pkg.bar.orange.nine.alpha"), | ||
| } | ||
| for importer, imported in imports: | ||
| graph.add_import(importer=importer, imported=imported) | ||
|
|
||
| result = graph.nominate_cycle_breakers("pkg.bar.orange") | ||
|
|
||
| assert result == imports | ||
|
|
||
| def test_on_package_with_one_child(self): | ||
| graph = self._build_acyclic_graph() | ||
| graph.add_module("pkg.bar.orange.ten.gamma.onechild") | ||
|
|
||
| result = graph.nominate_cycle_breakers("pkg.bar.orange.ten.gamma") | ||
|
|
||
| assert result == set() | ||
|
|
||
| def _build_graph_with_no_imports(self) -> ImportGraph: | ||
| graph = ImportGraph() | ||
| for module in ( | ||
| "pkg", | ||
| "pkg.foo", | ||
| "pkg.foo.blue", | ||
| "pkg.foo.blue.one", | ||
| "pkg.foo.blue.two", | ||
| "pkg.foo.green", | ||
| "pkg.bar", | ||
| "pkg.bar.red", | ||
| "pkg.bar.red.three", | ||
| "pkg.bar.red.four", | ||
| "pkg.bar.red.five", | ||
| "pkg.bar.red.six", | ||
| "pkg.bar.red.seven", | ||
| "pkg.bar.yellow", | ||
| "pkg.bar.yellow.eight", | ||
| "pkg.bar.orange", | ||
| "pkg.bar.orange.nine", | ||
| "pkg.bar.orange.nine.alpha", | ||
| "pkg.bar.orange.nine.beta", | ||
| "pkg.bar.orange.ten", | ||
| "pkg.bar.orange.ten.gamma", | ||
| "pkg.bar.orange.ten.delta", | ||
| ): | ||
| graph.add_module(module) | ||
| return graph | ||
|
|
||
| def _build_acyclic_graph(self) -> ImportGraph: | ||
| graph = self._build_graph_with_no_imports() | ||
| # Add imports that make: | ||
| # pkg.foo -> pkg.bar | ||
| # pkg.bar.yellow -> pkg.foo.red | ||
| # pkg.bar.orange.nine -> pkg.bar.orange.ten | ||
| for importer, imported in ( | ||
| ("pkg.foo", "pkg.bar.red"), | ||
| ("pkg.foo.green", "pkg.bar.yellow"), | ||
| ("pkg.foo.blue.two", "pkg.bar.red.three"), | ||
| ("pkg.foo.blue.two", "pkg.bar.red.four"), | ||
| ("pkg.foo.blue.two", "pkg.bar.red.five"), | ||
| ("pkg.foo.blue.two", "pkg.bar.red.six"), | ||
| ("pkg.foo.blue.two", "pkg.bar.red.seven"), | ||
| ("pkg.bar.yellow", "pkg.bar.red"), | ||
| ("pkg.bar.yellow.eight", "pkg.bar.red.three"), | ||
| ("pkg.bar.yellow.eight", "pkg.bar.red.four"), | ||
| ("pkg.bar.yellow.eight", "pkg.bar.red.five"), | ||
| ("pkg.bar.orange.nine", "pkg.bar.orange.ten.gamma"), | ||
| ("pkg.bar.orange.nine.alpha", "pkg.bar.orange.ten.gamma"), | ||
| ("pkg.bar.orange.nine.beta", "pkg.bar.orange.ten.delta"), | ||
| ): | ||
| graph.add_import(importer=importer, imported=imported) | ||
| return graph |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
I can see a risk related to only returning minimum weighted feedback arc set out of all found feedback arc sets. If for example we have a following cycle:
root.modelimporting fromroot.gui(2 imports -> 2 "points")root.guiimporting fromroot.model(3 imports -> 3 "points")we are suggesting to remove
root.modeldependencies fromroot.gui, but the other way around makes more sense. Maybe we should return sorted list of feedback arc sets together with "scores". Potentially, we could introduce a parameter "max_feedback_arc_sets".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.
I agree that this approach is will not necessarily find the right imports to remove. But to return all of them could potentially be computationally expensive and the number of potential sets of cycle breakers could be very large. The main aim here is to provide a meaningful, deterministic result in the case of a cyclic graph.
I think there would be scope for further tooling to try to arrive at a sensible layering based on a user's model of the domain. For example they might want to specify a non-exhaustive layering (e.g. 'gui is above models') and then this returns cycle breakers based on that additional constraint. That could be added as an optional parameter in a later version.
By the way, IMO helping users decide on the best layer is would probably belong better in Impulse, rather than Import Linter. That has a visual component and I can imagine providing more of a UI to help find the best layering (and even output a draft Import Linter contract).