Skip to content

Conversation

@pradeep90
Copy link
Contributor

@pradeep90 pradeep90 commented Mar 12, 2025

Override all the things!

The previous PR (#2061) allowed users to specify the container strategy. However, there's still a snag: the plugin hardcodes special strategies for TypeVar and Callable. These cannot be overridden by users, which is a problem when a law needs some generic value other than the container under test.

So, this PR allows users to override any of the strategies used by the plugin. It decouples the test-generation code from the hardcoded strategies by using a first-class value of Settings. The hardcoded strategies are in the default values of Settings and can be overridden without trouble.

Rationale

Why accept extra strategies within check_all_laws? Why not ask the user to register the strategies with hypothesis in the user's test file?

Our hypothesis plugin doesn't run tests. It creates tests and sticks them in the user's module.

So, even if I try:

with type_resolver.strategies_for_types({...}): 
     check_all_laws(MyContainer)

that sets the context only when defining the tests, not when running them. When the tests are run, the types are no longer mapped to the strategies. We could ask the user to set the context when pytest runs the tests, but that would be error-prone and tedious for the user. So, we have to set the context inside the created test. Hence this PR.

Why accept extra strategies?

Because the alternative is to write them to the hypothesis global variable, and global variables are evil.

Why can't the user register a strategy for KindN directly? It uses TypeVarTuple, which hypothesis doesn't understand. So, hypothesis thinks KindN is not a "subclass" of KindN[int, str, bool], because the number of type arguments don't match. Sad.

As a workaround, we need to write those kinds of laws as x: Callable[[], KindN[F, ...]] instead of x: KindN[F, ...]. That way, at least our Callable override can catch these types.

Implementation

  • laws.py:
    • Replace the impure context managers with a pure Settings value.
    • Split Settings into a default value (with the hardcoded strategies) and a user-provided value.
      • We merge these two values. (Monoid would have been useful for this - hint hint :) )
  • The rest is docs and tests.
    • I've documented Settings inline. I believe it will be auto-included in the docs page.

Test Plan

Run test_custom_strategy_for_callable.py after adding print statements to see the generated examples. (I don't know of other ways to see the examples in this case, since there is so much dynamically added context.)

pytest returns/ tests/ -vv --log-level INFO -m "hypothesis" -rP --pdb -k "test_custom_strategy_for_callable"

Before

The parameter container: KindN[_F_Applicative, ...] is always bound to a _Wrapper even though _Wrapper is not even an Applicative! (This is because _Wrapper <: KindN, sadly.)

some_identity <Wrapper: None> container <Wrapper: None>
some_identity <Wrapper: 0> container <Wrapper: 0>
some_identity <Wrapper: -27749> container <Wrapper: -116>
some_identity <Wrapper: False> container <Wrapper: False>
some_identity <Wrapper: False> container <Wrapper: True>
some_identity <Wrapper: b''> container <Wrapper: b''>

After

Pass in a strategy for Callable. Now, we get container as Maybes and Results.

some_identity <Wrapper: 0> container <Nothing>
some_identity <Wrapper: 25634> container <Some: None>
some_identity <Wrapper: -16697> container <Success: b'v\xf0'>
some_identity <Wrapper: -16697> container <Success: None>
some_identity <Wrapper: -95> container <Some: 3>
some_identity <Wrapper: -95> container <Some: 0>
some_identity <Wrapper: 2> container <Success: 2>
some_identity <Wrapper: 2> container <Some: 2>
some_identity <Wrapper: -15533> container <Some: 1.5>
some_identity <Wrapper: -15533> container <Some: 1.5>
some_identity <Wrapper: 3> container <Success: 0.0>
some_identity <Wrapper: 3> container <Some: 0.0>
some_identity <Wrapper: 5739> container <Failure: 12655>

Checklist

  • I have double checked that there are no unrelated changes in this pull request (old patches, accidental config files, etc)
  • I have created at least one test case for the changes I have made
  • I have updated the documentation for the changes I have made
  • I have added my changes to the CHANGELOG.md

@codecov
Copy link

codecov bot commented Mar 12, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (82ef3ef) to head (c1b78f2).
Report is 347 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##            master     #2066    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           80        81     +1     
  Lines         2485      2576    +91     
  Branches       437        44   -393     
==========================================
+ Hits          2485      2576    +91     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

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

Thanks, the work you are doing is really impressive. Please, let me know when to make a new release.

Checking laws is not compatible with ``pytest-xdist``,
because we use a lot of global mutable state there.
Please, use ``returns_lawful`` marker
to exclude them from ``pytest-xdist`` execution plan.
Copy link
Member

Choose a reason for hiding this comment

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

We can also make a note about running them on a single node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, this was an existing paragraph that I moved up. I don't have anything to add here.

check_all_laws(
_Wrapper,
container_strategy=st.builds(_Wrapper, st.integers()),
other_strategies=other_strategies,
Copy link
Member

Choose a reason for hiding this comment

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

maybe register_strategies? other feels a bit off to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. How about type_strategies?

@pradeep90
Copy link
Contributor Author

Thanks, the work you are doing is really impressive. Please, let me know when to make a new release.

Thanks! I appreciate your speedy responses too. Really helps keep the cycle time low.

I'll test-drive it for another week or so, just to make sure I don't run into any more issues that need fixing :)

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

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

Thank you!

@sobolevn sobolevn merged commit 2f4f6b5 into dry-python:master Mar 12, 2025
23 of 24 checks passed
@pradeep90
Copy link
Contributor Author

Please, let me know when to make a new release.

@sobolevn I didn't run into any more problems :) We should be good for a new release. Thanks!

@sobolevn
Copy link
Member

Awesome! Scheduled for tomorrow.

@sobolevn
Copy link
Member

0.25.0 is released 🎉
Thanks a lot for your contributions!

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants