Skip to content
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

Fix calling delete on instances using nested ModelMocker context managers #187

Merged
merged 1 commit into from
Sep 11, 2024

Conversation

nfantone
Copy link
Contributor

@nfantone nfantone commented Sep 10, 2024

Fixes #186.

Ok, this was more involved than I anticipated.

As explained in #186, .delete() operations on model instances within the scope of more than one ModelMocker is currently broken.

with ModelMocker(Manufacturer), ModelMocker(Car, outer=False):
    car = Car.objects.create(speed=10)
    car.delete()

    manufacturer = Manufacturer.objects.create(name='foo')
    manufacturer.delete() # <-- Calls original Django delete method instead of deleting mock

The above would also fail for any model other than Car, which is the last ModelMocker in the context chain.

The underlying reason for this is the fact that a ModelMocker sets up two global or "shared" mocks for a pair of methods in django.db.models.deletion.Collector: collect and delete. They are re-used across all Django model instances to handle deletion, regardless of whether they have been mocked or not.

If declaring nested ModelMocker context managers, each one will attempt to set up the same "shared" mocks, overriding the last one. However, the logic in place today is flawed as, even though these particular mocks are supposed to be global,

  • they rely on local state and MockSet objects to resolve the delete() query.
  • they fallback to invoking the original Django methods if the class of the model being deleted does not match the class used to instantiate the last ModelMocker (i.e.: the innermost context manager or decorator). This is not right as .delete() may be called from any one mocked model.

In addition,

  • a caller failing to pass outer=False to all mockers except the first, will cause shared patchers to be prematurely stopped for all managers and models, likely resulting in failed tests.
  • ModelMocker instances are not thread-safe and the result of running tests in parallel that rely on them is undetermined.
  • Their internal state is shared between unit tests and is never reset after being initialised.

In order to fix this, I initially attempted to patch the existing logic — but, while doing so and trying to understand the implementation, I realised I couldn't fully grasp the original reason why these mocks needed to be "global" in the first place. I noticed this was done +7y ago by @stphivos, so I'm sure there were some factors behind the decision. There may be something that I'm missing here — please, let me know if there is.

In the end, I opted for simplifying the whole thing:

  • removed all "shared" mocks completely;
  • outer=False is no longer needed (passing it now is a no-op);
  • declared a local mock for the delete method;
  • explicitly mocked models will rely on MockSet to resolve their queries, while any others will continue using Django as normal/expected.

By removing any shared state, nesting and mixing any number of ModelMocker managers becomes a non-issue and the result is always predictable.

@nfantone nfantone force-pushed the many-model-mocker-delete branch from 9fa3ec0 to 1b364eb Compare September 10, 2024 10:16
@nfantone
Copy link
Contributor Author

@stefan6419846 Apologies for the push, but I'm trying to get this over the line.

Do you think this could be reviewed? I've successfully applied changes from this branch, along with all others I've recently contributed, to my local project at work which features a large Django codebase. Having this merged would be very helpful.

@stefan6419846
Copy link
Collaborator

You have mentioned some possible design decisions from quite some years ago in your initial comment where you are not sure (as well as me being not sure about). Thus I hesitated to merge this for now.

@nfantone
Copy link
Contributor Author

Thanks. Yes, I did bring up issues with an old implementation. That's why I was looking for a review — not expecting this to be immediately merged. Is it that you'd like @stphivos to take a look?

But also, if left unaddressed, it means that very basic Django operations, like delete, are not fully supported by the library.

The more I look at it, the more it seems to me that this is (and has always been) an accidental omission.

@stefan6419846
Copy link
Collaborator

For me no direct oddities became visible with your change. Let's merge this for now. If it turns out that we indeed need further changes on this, we can always revisit this with some follow-up changes later on.

@stefan6419846 stefan6419846 merged commit 978e030 into stphivos:master Sep 11, 2024
14 checks passed
@nfantone nfantone deleted the many-model-mocker-delete branch September 11, 2024 23:46
@nfantone
Copy link
Contributor Author

@stefan6419846 Thank you for this.

Any chance we can get this and all other new additions released soon?

@stefan6419846
Copy link
Collaborator

As there is not release automation for PyPI and I do not have the necessary permissions, we will have to wait for @stphivos in each case.

@nfantone
Copy link
Contributor Author

@stphivos Would you be able to help here, please?

@nfantone
Copy link
Contributor Author

@stphivos Ping? I could really use a new release with all recent additions.

@nfantone
Copy link
Contributor Author

nfantone commented Sep 26, 2024

@stefan6419846 Hi. Any way you could support here? Maybe you have another way of contacting @stphivos?

Otherwise, I'll fork the repo and try publishing it myself.

@stphivos
Copy link
Owner

@nfantone apologies for the delay i've been incredibly busy lately. I will push a new release later today and also check how this can be automated with PyPI for future releases.

@nfantone
Copy link
Contributor Author

@stphivos Thank you so much. Apologies for the push — I appreciate people are busy. It was just that changes are critical for our internal work.

Let me know if I can help out.

@stefan6419846
Copy link
Collaborator

@nfantone I have upload permissions for PyPI now and I am going to try getting the release done in the next hours. For the future, we might switch to trusted publishing, but this requires additional setup and I do not want to force anyone to work more than possible.

@stefan6419846
Copy link
Collaborator

Okay, there apparently already has been a release, at least partly. PyPI and GitHub should be in sync now.

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.

Delete does not work with nested ModelMocker
3 participants