Repository navigation
Add broken contract guidance - #371
Conversation
| if not instructions: | ||
| return | ||
|
|
||
| output.print_warning("How to fix this:") |
There was a problem hiding this comment.
Should this be warning-yellow, or a new colour?
Yellow is already the Warnings section's colour, so this may briefly read as "this contract emitted a warning" when it didn't. Alternatives: leave it unstyled (it'd be the only non-red text in the block, so it stands out anyway), or add a dedicated style for guidance. Happy to change it.
There was a problem hiding this comment.
Let's just print it as-is for now (i.e. output.print). I think it works better with the way the contracts currently style their breakage messages.
| allow_indirect_imports = fields.BooleanField(required=False, default=False) | ||
| unmatched_ignore_imports_alerting = fields.EnumField(AlertLevel, default=AlertLevel.ERROR) | ||
| as_packages = fields.BooleanField(required=False, default=True) | ||
| instructions = fields.ListField(subfield=fields.StringField(), required=False) |
There was a problem hiding this comment.
instructions is the name from the issue and I'm not attached to it — how_to_fix is more direct about who it's for.
Note the config key and the rendered heading needn't match: the key serves whoever writes the contract, the heading serves whoever broke it. Hence instructions + "How to fix this:".
There was a problem hiding this comment.
I think I prefer the term 'guidance' instead of instructions. It would also be nice to be more specific about the context in which this will be shown.
What do you think about calling it breakage_guidance or broken_contract_guidance?
There was a problem hiding this comment.
I think I prefer broken_contract_guidance I'm going with that in the next iteration!
|
|
||
| output.new_line() | ||
|
|
||
| contract_utils.render_instructions(self.instructions) # type: ignore |
There was a problem hiding this comment.
Called at the same position in all five contracts deliberately, so the follow-up that moves this into rendering.py produces identical output.
seddonym
left a comment
There was a problem hiding this comment.
Sorry for the delay in reviewing. I like it!
Have left some small comments, let me know if you have any questions or thoughts.
Thanks @abeaufays !
| if not instructions: | ||
| return | ||
|
|
||
| output.print_warning("How to fix this:") |
There was a problem hiding this comment.
Let's just print it as-is for now (i.e. output.print). I think it works better with the way the contracts currently style their breakage messages.
| if not instructions: | ||
| return | ||
|
|
||
| output.print_warning("How to fix this:") |
There was a problem hiding this comment.
Let's omit this line. I think we should see the printed text as supplementary guidance that is added to the end of the built-in guidance. If someone wants to put 'How to fix this:' in their guidance then they can.
| allow_indirect_imports = fields.BooleanField(required=False, default=False) | ||
| unmatched_ignore_imports_alerting = fields.EnumField(AlertLevel, default=AlertLevel.ERROR) | ||
| as_packages = fields.BooleanField(required=False, default=True) | ||
| instructions = fields.ListField(subfield=fields.StringField(), required=False) |
There was a problem hiding this comment.
I think I prefer the term 'guidance' instead of instructions. It would also be nice to be more specific about the context in which this will be shown.
What do you think about calling it breakage_guidance or broken_contract_guidance?
| @@ -1,5 +1,9 @@ | |||
| # Release notes | |||
|
|
|||
| ## 2.14 (unreleased) | |||
There was a problem hiding this comment.
Can this line be changed to ## latest instead?
(Don't want to commit to that version necessarily.)
| skip_descendants: list[str] | None = None, | ||
| depth: str = "", | ||
| ignore_imports: list[str] | None = None, | ||
| instructions: list[str] | None = None, |
There was a problem hiding this comment.
Could we make this into a string by this point? The fact that it happens to be passed as a list is just a consequence of .ini right?
There was a problem hiding this comment.
I couldn't find a good way to do this so I'm implementing my TextField suggestion from the PR description for the next iteration.
| assert capture.get() == dedent( | ||
| """Illegal imports of protected package mypackage.foo.protected: | ||
|
|
||
| - mypackage.foo.sibling -> mypackage.foo.protected.models (l.6) |
There was a problem hiding this comment.
This could be indented for readability, like the other tests.
There was a problem hiding this comment.
I've done this in a last commit.
(I did write this test initially, oopsie)
TextField accepts a string or joins an INI reader's list of lines without discarding blank lines. Assistant-model: Claude Code
Allow built-in contracts to include optional guidance for resolving failures. Guidance is rendered after the contract's violations, so the person who introduced an import can see the intended alternative without reading the contract definition. The option uses TextField, accepting a multi-line value in INI or a TOML multi-line string while preserving blank lines. Assistant-model: Claude Code
These tests need to compare the result of the broken contract rendering against a static string. The static strings were not indented in this specific file.
3b2bada to
7d04198
Compare
|
@seddonym I've made the change you asked! |
This also fixes the INI/TOML selector.
seddonym
left a comment
There was a problem hiding this comment.
Looks great! Thanks.
I took the liberty of pushing a couple of tweaks to the docs, to make it more concise and to fix the INI/TOML switcher.
|
|
||
| def parse(self, raw_data: str | list) -> str: | ||
| if isinstance(raw_data, list): | ||
| return "\n".join(raw_data) |
There was a problem hiding this comment.
I wonder if it's worth calling textwrap.dedent on this so people can indent the text in the contracts, what do you think? (I don't think we need to document that, just a minor improvement.)
There was a problem hiding this comment.
I thought the indent was lost even before that step during .ini parsing 🤔
I'm trying on local and can't get it to work 😞
| ``` | ||
| - `broken_contract_guidance`: A message for Import Linter to display when the contract is broken, below the built-in message. (Optional.) | ||
|
|
||
| === "INI" |
There was a problem hiding this comment.
Oh I did not find this, oops!
Summary
broken_contract_guidanceto the five built-in contract types.TextFieldsupport for multi-line configuration text.Broken contract guidance
broken_contract_guidancelets whoever defines a contract explain how to resolve a failure, so the person whose change broke the contract can see the intended alternative without reading the contract definition.When a contract with guidance is broken, it is rendered after the violations:
One use case is migrating between systems: a contract can forbid the old system while signposting the new one, instead of leaving the reader to add another entry to the
ignore_importsburndown list.The field is declared on each built-in contract type, so custom contract types do not receive it for free yet. Making it framework-level would require
_get_field_names()to walk the MRO; it currently readscls.__dict__.Custom contracts can still use the contract_utils function to render it.
TextField
This PR also adds
TextField, a reusable field for one text value that may span several lines.That gives contract code a single string while retaining intentional paragraph breaks in INI configuration.
broken_contract_guidanceusesTextField; formatting, including bullets and wrapping, is the contract author's choice.Note:
.iniand.tomldon't have the exact same behaviour, we can't start a line with a space in an ini file, whereas we can with toml.Checklist
[x] Add tests for the change.
[x] Add documentation.
[x] Add release notes.
Related to #370
Disclaimer: made with Claude code