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

Improve generic subclass support #2549

Merged

Conversation

diabolo-dan
Copy link
Contributor

@diabolo-dan diabolo-dan commented Mar 19, 2021

Change Summary

Add parameterised subclasses to __bases__ when constructing new parameterised classes, so that A <: B => A[int] <: B[int].

This helps to facilitate a use case based on abstract classes, and leads to less unexpected behaviour when comparing between concrete versions of classes.

Related issue number

fix #2007

Checklist

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • changes/<pull request or issue id>-<github username>.md file added describing change
    (see changes/README.md for details)

@codecov
Copy link

codecov bot commented Mar 19, 2021

Codecov Report

Merging #2549 (26cba97) into master (7b7e705) will not change coverage.
The diff coverage is 100.00%.

❗ Current head 26cba97 differs from pull request most recent head 193ee06. Consider uploading reports for the commit 193ee06 to get more accurate results

@@            Coverage Diff            @@
##            master     #2549   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           25        25           
  Lines         5109      5133   +24     
  Branches      1050      1059    +9     
=========================================
+ Hits          5109      5133   +24     
Impacted Files Coverage Δ
pydantic/generics.py 100.00% <100.00%> (ø)
pydantic/main.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a35cde9...193ee06. Read the comment docs.

@diabolo-dan diabolo-dan force-pushed the improve_generic_subclass_support branch 3 times, most recently from 6ef9a8d to b731c84 Compare March 19, 2021 17:07
@diabolo-dan
Copy link
Contributor Author

test fastAPI is failling with:
ImportError: cannot import name 'RowProxy' from 'sqlalchemy.engine.result' (/opt/hostedtoolcache/Python/3.7.10/x64/lib/python3.7/site-packages/sqlalchemy/engine/result.py

As far as I can tell that's not anything I could've incidentally broken. I'm not sure what the best way to proceed with it would be?

@diabolo-dan diabolo-dan force-pushed the improve_generic_subclass_support branch 2 times, most recently from adb7c8e to 2d690f9 Compare March 22, 2021 13:30
@PrettyWood
Copy link
Member

@diabolo-dan Don't worry you can have a rest for the CI! ;)
It's due to fastapi tests that uses databases without pinning sqlalchemy dependency.
I already submitted a fix encode/databases#299 for that

pydantic/generics.py Outdated Show resolved Hide resolved
Copy link

@davidhyman davidhyman left a comment

Choose a reason for hiding this comment

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

Some minor thoughts

pydantic/generics.py Outdated Show resolved Hide resolved
pydantic/generics.py Outdated Show resolved Hide resolved
tests/test_generics.py Outdated Show resolved Hide resolved
pydantic/generics.py Show resolved Hide resolved
pydantic/generics.py Outdated Show resolved Hide resolved
pydantic/generics.py Outdated Show resolved Hide resolved
pydantic/generics.py Outdated Show resolved Hide resolved
@diabolo-dan diabolo-dan force-pushed the improve_generic_subclass_support branch 2 times, most recently from 050b132 to 26cba97 Compare March 23, 2021 20:39
@diabolo-dan
Copy link
Contributor Author

I notice that there was significant generic implementation done by @dmontagu , including the line:

    assert concrete_model.__name__ == 'Model[int, BT][str]'

Which is I believe the only concrete claim about the behaviour of reapplication of partially parameterised generic models. (namely extending from the former).

This behaviour adds complexity to establishing parameterised bases, and makes for less (because a concrete type could be constructed through arbitrary many type variables). I also think it's less intuitive than always building from the base, (so that Model[int, BT][str] is Model[int, str]). The alternative isn't perfect, because there's still the issue of checking issubclass(Model[int, BT][str], Model[int, AT]) which for various assumptions on the TypeVars, should ideally hold, but I don't think it's possible to achieve that (at least through setting __bases__).

I'm just wondering if there was a particular reason for implementing it this way, or if it was just the simplest thing that met the requirements at the time.

@diabolo-dan diabolo-dan force-pushed the improve_generic_subclass_support branch 2 times, most recently from 0bc3564 to 39d5566 Compare April 1, 2021 10:05
Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

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

Honestly, generics.py scares the hell out of me. I don't pretend to remember/understand everything in there and I barely ever (never?) use generics myself.

The only consolation is that this isn't part of core validation, just isinstance checks. (I think?)

I've got a few notes and questions, but overall, and from what I can see it looks reasonable.

pydantic/generics.py Outdated Show resolved Hide resolved
pydantic/generics.py Outdated Show resolved Hide resolved
Comment on lines 191 to 205
elif cls in _assigned_parameters and base_model in _assigned_parameters:
# cls is partially parameterised but not from base_model
# e.g. cls = B[S], base_model = A[S]
# B[S][int] should subclass A[int], (and will be transitively via B[int])
# but it's not viable to consistently subclass types with arbitrary construction
# So don't attempt to include A[S][int]
continue
elif cls in _assigned_parameters and base_model not in _assigned_parameters:
# cls is partially parameterized, base_model is original generic
# e.g. cls = B[str, T], base_model = B[S, T]
# Need to determine the mapping for the base_model parameters
mapped_types: Parametrization = {
key: typevars_map.get(value, value) for key, value in _assigned_parameters[cls].items()
}
yield from build_base_model(base_model, mapped_types)
Copy link
Member

Choose a reason for hiding this comment

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

can't these two cases be covered by

elif cls in _assigned_parameters:
    if base_model in _assigned_parameters:
        ...
    else:
        ...

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thinking was that the 2 cases were in some sense only incidentally related, so it helped the clarity to have them as top level cases.

I've moved them back to being under a common _assigned_parameters case.

pydantic/generics.py Show resolved Hide resolved
@samuelcolvin
Copy link
Member

please update.

diabolo-dan and others added 8 commits May 17, 2021 16:00
The type was incorrectly being picked up for this style of subclassing,
and it can be regardless inferred through cls.
* Improve parameterisation explanation
* fix typos
* Alias Parameterisation type
* start docstring with newline.
* Use None as default over empty tuple.

Co-authored-by: Samuel Colvin <samcolvin@gmail.com>
@diabolo-dan diabolo-dan force-pushed the improve_generic_subclass_support branch from 9f1d7ab to b4a8841 Compare May 17, 2021 15:00
@diabolo-dan diabolo-dan removed their assignment May 17, 2021
@diabolo-dan
Copy link
Contributor Author

please review.

@samuelcolvin samuelcolvin merged commit e71f53d into pydantic:master Dec 5, 2021
@samuelcolvin
Copy link
Member

great, thank you.

PrettyWood pushed a commit to PrettyWood/pydantic that referenced this pull request Dec 6, 2021
* Derive concrete subclasses for parameterised generics

* Resolve type issues

* Add negative assertions to generic subclass tests

* Remove incorrect subclassing of partial.

The type was incorrectly being picked up for this style of subclassing,
and it can be regardless inferred through cls.

* Apply feedback:

* Improve parameterisation explanation
* fix typos
* Alias Parameterisation type

* Apply suggestions from code review

* start docstring with newline.
* Use None as default over empty tuple.

Co-authored-by: Samuel Colvin <samcolvin@gmail.com>

* Combine _assigned_parameters cases in __paramaterized_bases__ of generics

* Add description for the `_assigned_parameters` variable.

Co-authored-by: Samuel Colvin <samcolvin@gmail.com>
Co-authored-by: Samuel Colvin <s@muelcolvin.com>
@samuelcolvin
Copy link
Member

Just a note for anyone watching this PR, this changes here I think have caused a memory build up, see #3829.

To mitigate this, I've implemented (and will soon merge) #4083 which limits the size of _generic_types_cache and _assigned_parameters_limit to 1000. These configured by settings generics._generic_types_cache_limit and generics._assigned_parameters_limit.

Feedback on the PR welcome, but in order to include this fix in v1.9.1 I won't being waiting for feedback before merging - any further changes would need to be in another PR.

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

Successfully merging this pull request may close these issues.

GenericModel - Recursive abstract class fails validation
5 participants