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
Apply profile dependencies only once. #16
Conversation
Dependency profiles from 'metadata.xml' that are already applied, are not applied again. Instead, apply upgrade steps for already applied dependency profiles. Added options always_apply_profiles and upgrade_dependencies to runAllImportStepsFromProfile, so you can tweak the behavior in code.
All functions that accept a profile id argument and only work when the id does *not* have this string at the start, will now simply strip it off if it is there. For example, 'getLastVersionForProfile' will give the same answer whether you ask it for the version of profile id 'foo:default' or 'profile-foo:default'. The various registries are storing 'foo:default' as profile id, but various functions return 'profile-foo:default' when reporting about this profile. Now you don't have to double or triple check yourself.
Can we test this without having to worry about Plone? |
@@ -644,6 +644,10 @@ def __init__(self): | |||
def getProfileInfo(self, profile_id, for_=None): | |||
""" See IProfileRegistry. | |||
""" | |||
if profile_id.startswith("profile-"): | |||
profile_id = profile_id[len('profile-'):] |
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.
Minor quibble but what's with len('profile-')
? profile_id[8:]
would make more sense to me as this is not dynamic.
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.
For me the len('profile-')
makes the intent more explicit. When checking whether the code is correct, you immediately see that this does the correct thing. With len(8)
I would start counting the number of characters in profile-
to see if those are really 8, and then do a recount to double check it.
At least, that is my reasoning. I don't mind changing it if wanted.
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 have the same code repeatedly, which smells of possible typos. Tests will determine whether it's working correctly and is likely to flag up errors precisely because of the fragility.
The alternative would be to use a regex for 'profile-|snapshot-'
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 don't see len(8)
being less fragile than `len('profile-').
Code like this has been here for years, see for example:
https://github.com/zopefoundation/Products.GenericSetup/blame/1.7.7/Products/GenericSetup/tool.py#L839
https://github.com/zopefoundation/Products.GenericSetup/blame/1.7.7/Products/GenericSetup/tool.py#L998
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.
Well, looking more closely, your comment was about two lines that both had profile-
, but after those lines are two similar lines with snapshot-
. I guess that was your point, but it got lost because I did not see those last two lines on github when reading your comment.
I'll make the code a bit clearer. :-)
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 in commit b9fe932.
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 point was that x[8:] is more fragile than x[len(somtheing):] So snapshot- and profile- don't get muddled by accident. I've not reviewed GenericSetup before so I'm bound to pick up on stuff.
@Themanwithoutaplan The example I give is for some Plone packages, but the same is true outside of Plone. |
for profile_id in chain: | ||
last_index = len(chain) - 1 | ||
for index, profile_id in enumerate(chain): | ||
if not always_apply_profiles and index != last_index: |
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.
Surely always_apply_profiles
should be outside the loop?
If chain has a length then for profile_id in chain[:-1]:
should work too, shouldn't 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.
No, always_apply_profiles
needs to be inside the loop. If it is False, then we do not need the check in the following lines, but can simple execute the original lines at the end of the loop (context =...; self.applyContext(context)
).
I need the enumerate because I need the index. Note that the last profile in the chain is the profile that you originally wanted to install (by selecting it in portal_setup / Import tab) and the earlier ones in the chain are its dependencies. For those dependencies we need the always_apply_profiles check
, for the original one not, because the user has explicitly requested to apply this profile.
@Themanwithoutaplan Can you elaborate about why you think Plone is something that does need to be tought of then working on Genericsetup? |
@do3cc I just want to be able to check this without reference to Plone. |
@Themanwithoutaplan Oh, got you now :-) |
@mauritsvanrees You obviously have spent quite some time thinking about the implications of the changes, which I very much like, btw. |
# Maybe apply upgrade steps, if any, otherwise continue. | ||
if upgrade_dependencies: | ||
self.upgradeProfile(profile_id) | ||
continue | ||
context = self._getImportContext(profile_id, purge_old, archive) | ||
self.applyContext(context) |
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.
@Themanwithoutaplan Regarding the last comment, I think you are just missing that these two lines are still inside the loop. The should make the logic clearer.
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.
Thanks, the logic is still a bit weird but makes more sense now. This is the sort of method where I'd normally spend time working out how to reduce the number of branches in it but sometimes life is too short.
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.
That index != last_index means, not dependent profile is not easily visible from these lines of 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.
Fair enough. I have added an explanation in comments in 3f93d07.
@do3cc It is fine if an upgrade step has code to apply a specific import step or a complete profile, either its own profile or a third party profile. But the idea is: if you apply a complete profile (in code or ZMI or a control panel) and this profile has dependency profiles that are already applied, then with this pull request these dependency profiles are no longer reapplied. In case those dependency profiles have any upgrade steps that need to be applied, then those upgrade steps are applied. There is no need for changing best practices. |
@mauritsvanrees Currently, my upgrade steps often just load a step from my default profile, because profiles get f uped all the times anyway... |
I'd be willing to press the merge button, but wait for @Themanwithoutaplan responses |
@do3cc Applying import steps within an upgrade step is fine in itself. I do it too. This pull request changes nothing about that. You just always need to watch out that this does not override manual changes or changes made by other packages, like inadvertently resetting properties to their defaults. If that is a danger in the package you are working on, you can always write python code in the upgrade step. The best practice will depend on your use case, and of course you have less worry if this is only an internal package for one site of one client. |
The number of codepaths in Take the opportunity to look a bit more around the code. The parser is still using SAX. :-( |
@mauritsvanrees This changes my best practice because before it simply was not possible to have good practices :-) |
@Themanwithoutaplan I am all in favour of declaring the currently existing complexity a bug. But I fear nobody is going to work on it and it would be unfair to block the PR because of it. What is your suggestion? |
@do3cc I think that at least the new codepaths should be in separate methods but I'd be tempted to go further. The possible dependency on the side-effects of a mutable keyword arg (as opposed to an instance variable) makes me physically unwell. |
@Themanwithoutaplan Ah, you didn't see the places where attribute getter trigger changes then :-) |
No, I spent 10 minutes trying to grok the original method and then the changes to it. We've got attribute magic as well to contend with? Oh joy! I suspect much of the code could probably be simplified but I also suspect, like you, that no one can be bothered. |
@Themanwithoutaplan If @mauritsvanrees Won't say anything, I'll clean up the method during the week |
This was a leftover from an earlier refactoring in commit 6448157.
@Themanwithoutaplan I did not know about I noticed that the Putting my new code path in a separate method is not going to work, I think. In 1 Gather a chain of profiles. Actually, seems good to add this in comments in that method. Certainly clarification is good. Done. The second item is the core one and I think it is as complex as it needs to be. We could split the entire With an extra method it becomes more complex due to all the methods that are called. Especially I do not like that we constantly have to juggle keyword arguments around. You call Well, we could split out the original part of the
But then you have three methods that all need to return some messages for its caller to handle, which also feels awkward. |
…rofile. This means we have one keyword argument less, and there are no two options that can conflict. In the ZMI you can choose between four strategies for dealing with dependencies. The actions in the Import tab are explained better.
After a small discussion on the cmf mailing list, I have updated the pull request. See last commit a8ec44f. The result is best shown with a screen shot of the Import tab. It makes the strategies available in the ZMI. And it makes it obvious which option influences which button, because the original form was unclear. |
👍 this is a huge improvement! |
The new "strategy" choices look reasonable to me. I've lost the thread about backward-compatibility, though: how will this change affect existing code which calls |
runAllImportStepsFromProfile:
|
I really like the new flexible variant. I personally run often in problems, because the depended profiles were applied. We worked around with custom steps applying dependencies using a blacklist. This always felt wrong. Only running upgrade steps feels much cleaner and really solve the problem we worked around. |
No objections anymore on the CMF mailing list. |
…nly-once Apply profile dependencies only once.
@mauritsvanrees Thanks for your work on this one! |
Example scenario:
Problems with this scenario:
I would like to prevent this. So this is the change:
Dependency profiles from
metadata.xml
that are already applied, are not applied again. Instead, its upgrade steps, if any, are applied. In code you can choose the old behavior of always applying the dependencies, by callingrunAllImportStepsFromProfile
withalways_apply_profiles=True
. Or you can choose to be happy with any applied version and ignore any upgrade steps, by usingupgrade_dependencies=False
. Note that these arguments cannot both beTrue
.Note: I could theoretically enhance
metadata.xml
so you can tweak the behavior:But this would require changing the output of
getDependenciesForProfile
andgetProfileInfo
, so probably best not to do that.While doing this, I got confused about which GenericSetup method needs a profile id with
profile-
at the start, and which needs a profile id without it. So I added a second change:Be more forgiving when dealing with profile ids with or without
profile-
at the start. All functions that accept a profile id argument and only work when the id does not have this string at the start, will now strip it off if it is there. For example,getLastVersionForProfile
will give the same answer whether you ask it for the version of profile idfoo
orprofile-foo
.These two changes of this pull request are in two commits, with minimal overlap, so if needed it should be fairly easy to cherry-pick only one.