-
Notifications
You must be signed in to change notification settings - Fork 17
Closed layers #227
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 layers #227
Conversation
CodSpeed Instrumentation Performance ReportMerging #227 will not alter performanceComparing Summary
|
db30b9a to
2407f38
Compare
| } | ||
| } | ||
|
|
||
| // Should not be imported by higher layers if there is a closed layer inbetween. |
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.
@seddonym I'm unsure if this is the definition of closed layers that we want. Is this behaviour consistent with your understanding of the term "closed layer"?
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.
The test_cannot_import_through_closed_mid makes the implemented behaviour clear.
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.
Yes I think so. The question of whether to disallow indirect imports is interesting though, I wonder if that is something we would need to make optional. Let me have a think about it.
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.
Coming back to it, I agree with how you've done it here. Can you see any reason why we should allow indirect imports?
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 with you.
Can you see any reason why we should allow indirect imports?
I think allowing indirect imports would undermine the point of the contract. I think indirect imports should be disallowed. If we find that for some reason we need that in the future, then we could always add that option then (but by default I think indirect imports should be disallowed).
seddonym
left a comment
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.
Great to see this taking shape!
I haven't reviewed the implementation in detail yet, but I like what I see so far. I do think we should adjust it so that the field on the Layer is closed (default False), not open (default True). This is because I want closed layers to feel like more of a special case.
Happy to discuss if you feel strongly the other way.
|
Looks like there is a related issue in import linter seddonym/import-linter#245 |
seddonym
left a comment
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.
This is great stuff. I have hardly anything to add, other than it would be good to sense check the behaviour when there are multiple closed layers - and add tests for that.
src/grimp/domain/valueobjects.py
Outdated
|
|
||
| def __str__(self) -> str: | ||
| return f"{self.module_tails}, independent={self.independent}" | ||
| return f"{self.module_tails}, independent={self.independent}, closed={self.closed}" |
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.
Could we have test coverage for this?
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.
Done.
CHANGELOG.rst
Outdated
| Unreleased | ||
| ---------- | ||
|
|
||
| * Implement closed layers - a feature that prevents imports from higher layers |
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.
Could we change this to Add closed layers to layer contract. - and skip the rest?
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.
Done.
|
|
||
| let permutations = graph.generate_module_permutations(&levels); | ||
|
|
||
| // Modules in independent layer should not import each other |
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.
Out of interest, why not just do a single assertion on what the result is?
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.
LLM generated it this way 😅 Updated now.
|
|
||
| let permutations = graph.generate_module_permutations(&levels); | ||
|
|
||
| assert_eq!( |
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.
Personally I think this would be clearer as a single assertion on the result.
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.
| } | ||
| } | ||
|
|
||
| // Should not be imported by higher layers if there is a closed layer inbetween. |
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.
Coming back to it, I agree with how you've done it here. Can you see any reason why we should allow indirect imports?
| return zip(a, b) | ||
|
|
||
|
|
||
| class TestClosedLayers: |
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 think we're missing tests for layers with multiple closed layers, right?
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.
Maybe. I'm unsure exactly what behaviour we want to test here. I've added another unit test case for generate_module_permutations for two closed layers - does this cover what you were hoping to test?
|
|
||
| let levels = vec![top_level, middle_level, bottom_level]; | ||
|
|
||
| let permutations = graph.generate_module_permutations(&levels); |
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.
Coming to this function afresh, makes me think it's not very well named (or at least could do with more documentation) - do you agree?
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.
True - renamed
099420e to
c30793d
Compare
|
|
||
| def __str__(self) -> str: | ||
| return f"{self.module_tails}, independent={self.independent}" | ||
| module_tails = sorted(self.module_tails) |
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.
This sorted needed to make the test stable.
The returned order isn't stable anyway, and order doesn't matter.
|
Hi @seddonym - this is ready for another look now. Sorry the commit history is a bit messy. |
seddonym
left a comment
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.
Nearly there - I do think it's important that we have reasonably thorough Python tests for this though. Thanks for all your work on it so far!
| } | ||
|
|
||
| fn generate_module_permutations( | ||
| fn generate_illegal_import_permutations_for_layers( |
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.
Definitely better - might be worth adding a comment to explain what the tuple corresponds to (e.g. is the first one the source and the second one the module that should not be imported by it)?
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.
Added a comment in the docstring now.
| } | ||
|
|
||
| #[test] | ||
| fn test_generate_module_permutations_two_closed_layers() { |
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 think we should have Python tests for this too (or instead of) - one of the principles of the project is that the Python tests are pretty comprehensive.
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.
Okay sure - python tests extended now.
|
|
||
| let top_level = Level::new(top_layer, false, false); | ||
| let middle_top_level = Level::new(middle_top_layer, false, true); // Closed | ||
| let middle_bottom_level = Level::new(middle_bottom_layer, false, true); // Closed |
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.
Good to have a test for two closed layers together, but feels like we could also do with something along these lines
one
two
three (closed)
four
five
six (closed)
seven
eight
I'd put that in the Python test though probably.
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.
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 tested this case:
layers = [
Layer("highest"),
Layer("high", closed=True),
Layer("mid"),
Layer("low", closed=True),
Layer("lowest"),
]
I'm not sure what benefit we'd get from testing even more layers, so kept it simple (in your example you put two open layers "four" and "five" between the closed layers) 🤔
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 was just thinking about testing boundaries, but yes this is much better now. Thank you!
b716fff to
7e2c3ae
Compare
seddonym
left a comment
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.
Looks great. Thanks so much for this!
|
I imagine you will probably be thinking about adding this to Import Linter - and therefore when the next Grimp release will be. I am considering delaying the release until I have done the follow up work from #229, but I could be talked around 😄. |
(AI assisted PR description)
Implement Closed Layers Feature
Overview
This PR introduces the concept of "closed layers" to Grimp, providing an additional constraint mechanism for controlling imports between modules in a Python codebase.
Key Behaviour
When a layer is marked as closed, modules in higher layers cannot import modules that exist below the closed layer. This creates a strict boundary that forces higher layers to interact only with the closed layer itself, not with anything beneath it. This is particularly useful for enforcing clean architectural boundaries where lower-level implementation details should be hidden from higher-level components.
Example Use Case
Consider this layered architecture:
With
mid_levelconfigured as a closed layer:high_levelcan import frommid_levelhigh_levelcannot import fromlow_level(must go throughmid_level)mid_levelcan import fromlow_levelThis enforces that
high_levelmodules can only interact withlow_levelfunctionality through themid_levelinterface, preventing dependency leakage and maintaining clear architectural boundaries.