-
Notifications
You must be signed in to change notification settings - Fork 664
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
Enable Lintables to be modified #1884
Conversation
d9d963c
to
b7b7b9d
Compare
Where are these streams closed? If they aren't, this will cause |
The current implementation relies on StringIO so closing them is not an issue. If we need to switch to temporary files then we'd probably need to auto open/close them or something. I'm not sure where. |
At least to me, it would be clearer to rename Could you also provide some guidance on when we would use Could you also talk about why you went with a method to expose a writable stream, rather than a method to set the updated content? I understand why we're using streams over string; but why have I'm excited about rules being able to remediate Ansible, and I think this lays important groundwork for that feature. |
Lintable.writable
stream and Lintable.updated
indicatorLintable.content
and add Lintable.updated
indicator
OK. I changed the implementation from a |
The Lintable could represent a new file or it could be updating an existing file. Either way, this allows us to detect file updates. And it allows us (in the future) to provide some kind of diff-style output for those changes.
989b8e4
to
784fe7a
Compare
Lintable.content
and add Lintable.updated
indicatorLintable.content
setter and deleter + add Lintable.updated
indicator
Why are the comments minimized? |
Because they were about the old implementation. You asked about streams, for example, which this does not use any more. So I marked the comments as outdated. |
I don't think we need to do this with legitimate comments. It's harder to find them this way. We mostly hide spam / outdated bot comments but doing this doesn't generate any notifications decreasing the chances of finding out what happened and why. So it's also bad for transparency. If the conversations need to be disregarded, just leave a comment about that — it's probably the best way to keep the discussions/decisions traceable. Besides, GitHub already collapses big chunk of comments anyway. So when I open the thread to see the updates/answers to previous questions, it's rather confusing to see some approvals combined with "no reply" and "removed questions". Looks like this adds to the cognitive load of people trying to find own discussions that they started. |
okey dokey. unhid |
I do mark comments as outdated every-time they no longer apply to current implementation, just to keep the review size easy to read to other reviewers, including myself. The moment they become outdated, they become spam, and that is why github added these particular reasons when you hide them. AFAIK, @webknjaz is the only one that was intrigued by this. Maybe it is time to check with others from the team and see what think. I personally find long conversations hard to follow, hide option helps fighting that. |
Well, if there's disregarded comments without replies for no reason, it doesn't look good. I wasn't intrigued, I was only shocked by the disrespect. There is no indication informing me that everything has been updated and needs rechecking. The only indication is that there are some new commits without any context explaining that they contain different content. Anyway, I don't see why you disregard what I said — I merely described what signals I was getting and what I saw with my own eyes. I don't think you know better what I saw unless I explain, which I did. |
@cognifloyd thanks! For the future — it may be fine to hide things but then, take the responsibility of notifying the affected parties about this with an explicit comment and mentions that would trigger notifications and give people a chance to see what happened without forcing them to guess whether our judgment about their comments was reasonable. We can't always know upfront if what you thought another person meant is the same thing you perceived. Besides, we don't know/can't assume if the other party is caught up with the updates and making things disappear would inevitably cause extra cognitive load for those who care to understand how the context they may remember got transformed into a new one. |
lintable = Lintable("bad_type.yaml", BASIC_PLAYBOOK) | ||
assert lintable.content == BASIC_PLAYBOOK | ||
|
||
with pytest.raises(TypeError): |
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.
It is strongly recommended to always use the match=
argument for all pytest.raises()
/pytest.warns()
/pytest.deprecated()
uses to prevent accidental illegitimate passes.
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.
Doing that feels like relying on implementation details. Had I done that with the old implementation, I would have had to modify it with this implementation because the TypeError is now being raised in my code, but something else rose it with the old implementation. So, I don't like using the match=
argument.
This test is not checking for a message. It is checking that it raises a TypeError for non-str things. I don't care why or what message is used, so adding a match=
here is misleading about my intents when writing the code.
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.
Imagine you refactor that code and make a mistake in a function signature you call inside of the setter or down the stack. It will cause a TypeError
and the test will still pass with a bug merged in 🤷♂️
with lintable.path.open("w", encoding="utf-8") as f: | ||
f.write(content) |
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.
Why didn't you just do
with lintable.path.open("w", encoding="utf-8") as f: | |
f.write(content) | |
lintable.path.write_text(content) |
?
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.
Reasoning is in #1884 (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.
I think that you are referring to the .resolve()
call, which is something that is not happening here, no?
if not force and not self.updated: | ||
# No changes to write. | ||
return | ||
with open(self.path.resolve(), mode="w", encoding="utf-8") as file: |
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.
Why use an old-fashioned low-level API for this and not pathlib-native one?
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.
If a file is a symlink, I don't want to replace the symlink with an actual file, I want to update the contents of the symlink's target. I do not see pathlib methods for open or write that follow symlinks.
This matches the implementaiton introduced in #1770.
return self._content | ||
|
||
@content.setter |
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 use of a setter seems weird to me here. A natural goal of setters is to set values mostly but this one also does I/O and shuffles the internal state. It feels like this sort of thing would require an explicit method call.
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.
OTOH the whole idea of having a mutable object appears questionable — would it be possible to have some transformation API that would take one lintable and construct/return a new one in an immutable manner?
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 lintable instance is used all over the place. Making it immutable feels very odd. I would have to somehow make everything that accesses content via a Lintable check to see if something else has an in-memory change queued up to write to the file.
Feel free to open an alternative PR that shows how such an immutable object might work. I'm not willing to pursue that avenue.
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'm open to an alternative to the setter, but I'm tired of reimplementing this. Show me what interface you want instead of a setter.
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.
You're right, it may be hard to make it immutable with the current codebase, I won't insist on refactoring the whole project, of course :)
Have you considered exposing a method? (just thinking out loud)
2909b21
to
f544f32
Compare
@cidrblock Can you please help with this one? |
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 good.
The only comment I have, which is totally subjective and could go in this PR, in another PR or in the trash.......... and it's probably because I'm less familiar with the patterns in the repo.
I (personally) prefer to see all the class self.* attributes defined and typed at the top of the init, even without default values. I tend to look there first to see what something is when used later and it's type.... especially when I'm seeing code for the first time. Your call.
I think that would be a fine follow-up PR. I'm trying to minimize the refactoring in this PR. |
As none of the current opened comments is critical, I will merge it as is. We can address these in follow-ups, especially as it is likely that we would need to tune the implementation while we start using 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.
Apparently some of the comments were not posted yesterday. Submitting them now.
with open(self.path.resolve(), mode="w", encoding="utf-8") as file: | ||
file.write(self._content or "") |
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.
Oh, I didn't mean to suggest removing the resolution. I was thinking about something like
with open(self.path.resolve(), mode="w", encoding="utf-8") as file: | |
file.write(self._content or "") | |
self.path.resolve().write_text(self._content or "") |
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.
Huh. I thought resolve()
returned a string, not another Path object. Yeah, this would work.
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.
See #1913
with lintable.path.open("w", encoding="utf-8") as f: | ||
f.write(content) |
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 that you are referring to the .resolve()
call, which is something that is not happening here, no?
lintable = Lintable("bad_type.yaml", BASIC_PLAYBOOK) | ||
assert lintable.content == BASIC_PLAYBOOK | ||
|
||
with pytest.raises(TypeError): |
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.
Imagine you refactor that code and make a mistake in a function signature you call inside of the setter or down the stack. It will cause a TypeError
and the test will still pass with a bug merged in 🤷♂️
We need a way to write files and keep track of how we've changed them.
This prepares for formatting in transforms in #1828. Feedback from that PR suggests that we'll need at least a count of the files that have changed and possibly a diff view that shows how files have changed.
This adds a setter for
Lintable.content
which monitors the content for changes..When the content is changed, it sets
Lintable.updated = True
.Original content is also preserved to facilitate further comparison or resetting the content if needed.
There is also a deleter for
Lintable.content
so that if something needs to modify a file externally, the developer can trigger reloading the content from disk withdel lintable.content
.NB: I originally used a stream/file-like object because that's what
ruamel.yaml
's API expects. But that made the interface more confusing. The Transformer will have to create a StringIO instance on the fly when usingruamel.yaml
.Replaces #1865 (an alternative implementation)