Skip to content

Conversation

@Peter554
Copy link
Collaborator

@Peter554 Peter554 commented Jun 18, 2025

(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:

app/
  high_level/     # Higher layer
  mid_level/      # Middle layer (closed)
  low_level/      # Lower layer

With mid_level configured as a closed layer:

  • high_level can import from mid_level
  • high_level cannot import from low_level (must go through mid_level)
  • mid_level can import from low_level

This enforces that high_level modules can only interact with low_level functionality through the mid_level interface, preventing dependency leakage and maintaining clear architectural boundaries.

@codspeed-hq
Copy link

codspeed-hq bot commented Jun 18, 2025

CodSpeed Instrumentation Performance Report

Merging #227 will not alter performance

Comparing Peter554:closed-layers (5c6a9cf) with master (0abffb2)

Summary

✅ 22 untouched benchmarks

@Peter554 Peter554 marked this pull request as ready for review June 18, 2025 19:09
@Peter554 Peter554 force-pushed the closed-layers branch 2 times, most recently from db30b9a to 2407f38 Compare June 19, 2025 10:26
}
}

// Should not be imported by higher layers if there is a closed layer inbetween.
Copy link
Collaborator Author

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"?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

Copy link
Collaborator

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?

Copy link
Collaborator Author

@Peter554 Peter554 Jul 12, 2025

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).

Copy link
Collaborator

@seddonym seddonym left a 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.

@Peter554
Copy link
Collaborator Author

Looks like there is a related issue in import linter seddonym/import-linter#245

Copy link
Collaborator

@seddonym seddonym left a 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.


def __str__(self) -> str:
return f"{self.module_tails}, independent={self.independent}"
return f"{self.module_tails}, independent={self.independent}, closed={self.closed}"
Copy link
Collaborator

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?

Copy link
Collaborator Author

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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!(
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.
Copy link
Collaborator

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:
Copy link
Collaborator

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?

Copy link
Collaborator Author

@Peter554 Peter554 Jul 13, 2025

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);
Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True - renamed

@Peter554 Peter554 force-pushed the closed-layers branch 3 times, most recently from 099420e to c30793d Compare July 12, 2025 19:54

def __str__(self) -> str:
return f"{self.module_tails}, independent={self.independent}"
module_tails = sorted(self.module_tails)
Copy link
Collaborator Author

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.
@Peter554 Peter554 requested a review from seddonym July 13, 2025 09:45
@Peter554
Copy link
Collaborator Author

Hi @seddonym - this is ready for another look now. Sorry the commit history is a bit messy.

Copy link
Collaborator

@seddonym seddonym left a 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(
Copy link
Collaborator

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)?

Copy link
Collaborator Author

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() {
Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

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) 🤔

Copy link
Collaborator

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!

@Peter554 Peter554 force-pushed the closed-layers branch 2 times, most recently from b716fff to 7e2c3ae Compare July 16, 2025 05:13
@Peter554 Peter554 requested a review from seddonym July 16, 2025 06:33
Copy link
Collaborator

@seddonym seddonym left a 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!

@seddonym seddonym merged commit 01d0098 into python-grimp:master Jul 16, 2025
18 checks passed
@seddonym
Copy link
Collaborator

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 😄.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants