Skip to content

Commit 98da221

Browse files
committed
Fix unmatched variadic param, more tests
1 parent 17c9434 commit 98da221

File tree

2 files changed

+137
-49
lines changed

2 files changed

+137
-49
lines changed

crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md

Lines changed: 129 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -503,10 +503,20 @@ static_assert(is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFro
503503
static_assert(is_subtype_of(CallableTypeFromFunction[int_param_different_name], CallableTypeFromFunction[int_param]))
504504
```
505505

506+
Multiple positional-only parameters are checked in order:
507+
508+
```py
509+
def multi_param1(a: float, b: int, c: str, /) -> None: ...
510+
def multi_param2(b: int, c: bool, a: str, /) -> None: ...
511+
512+
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
513+
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
514+
```
515+
506516
#### Positional-only with default value
507517

508518
If the parameter has a default value, it's treated as optional. This means that the parameter at the
509-
corresponding position in the other function does not need to have a default value.
519+
corresponding position in the supertype does not need to have a default value.
510520

511521
```py
512522
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
@@ -522,7 +532,7 @@ static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], Callable
522532
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))
523533
```
524534

525-
As the parameter itself is optional, it can be omitted in the subtype:
535+
As the parameter itself is optional, it can be omitted in the supertype:
526536

527537
```py
528538
def empty() -> None: ...
@@ -532,9 +542,18 @@ static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], C
532542
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
533543
```
534544

545+
The subtype can include as many positional-only parameters as long as they have the default value:
546+
547+
```py
548+
def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ...
549+
550+
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param], CallableTypeFromFunction[empty]))
551+
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param]))
552+
```
553+
535554
#### Positional-only with other kinds
536555

537-
If a parameter is declared as positional-only, then the corresponding parameter in the subtype
556+
If a parameter is declared as positional-only, then the corresponding parameter in the supertype
538557
cannot be any other parameter kind.
539558

540559
```py
@@ -543,21 +562,15 @@ from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_asse
543562
def positional_only(a: int, /) -> None: ...
544563
def standard(a: int) -> None: ...
545564
def keyword_only(*, a: int) -> None: ...
546-
def variadic(*args: int) -> None: ...
547-
def keyword_variadic(**kwargs: int) -> None: ...
565+
def variadic(*a: int) -> None: ...
566+
def keyword_variadic(**a: int) -> None: ...
548567

549568
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[standard]))
550569
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_only]))
551570
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[variadic]))
552571
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_variadic]))
553572
```
554573

555-
But, a positional-only parameter can be a subtype of a standard parameter:
556-
557-
```py
558-
static_assert(is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[positional_only]))
559-
```
560-
561574
#### Standard
562575

563576
A standard parameter is either a positional or a keyword parameter.
@@ -601,22 +614,44 @@ static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], Callable
601614
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
602615
```
603616

604-
#### Standard with other kinds
617+
Multiple standard parameters are checked in order along with their names:
605618

606-
If the corresponding parameter in the subtype is a keyword-only parameter, it behaves in the same
607-
way. This is because keyword-only parameter is one of the kind of standard parameter.
619+
```py
620+
def multi_param1(a: float, b: int, c: str) -> None: ...
621+
def multi_param2(a: int, b: bool, c: str) -> None: ...
622+
623+
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
624+
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
625+
```
626+
627+
The subtype can include as many standard parameters as long as they have the default value:
628+
629+
```py
630+
def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ...
631+
632+
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param_default], CallableTypeFromFunction[empty]))
633+
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param_default]))
634+
```
635+
636+
#### Standard with keyword-only
637+
638+
A keyword-only parameter in the supertype can be substituted with the corresponding standard
639+
parameter in the subtype with the same name. This is because a standard parameter is more flexible
640+
than a keyword-only parameter.
608641

609642
```py
610643
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
611644

612645
def standard_a(a: int) -> None: ...
613646
def keyword_b(*, b: int) -> None: ...
614647

648+
# The name of the parameters are different
615649
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[keyword_b]))
616650

617651
def standard_float(a: float) -> None: ...
618652
def keyword_int(*, a: int) -> None: ...
619653

654+
# Here, the name of the parameters are the same
620655
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[keyword_int]))
621656

622657
def standard_with_default(a: int = 1) -> None: ...
@@ -627,15 +662,28 @@ static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], Cal
627662
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
628663
```
629664

630-
And, the same is for positional-only parameter except that the names are not required to be the
631-
same.
665+
The position of the keyword-only parameters does not matter:
666+
667+
```py
668+
def multi_standard(a: float, b: int, c: str) -> None: ...
669+
def multi_keyword(*, b: bool, c: str, a: int) -> None: ...
670+
671+
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_keyword]))
672+
```
673+
674+
#### Standard with positional-only
675+
676+
A positional-only parameter in the supertype can be substituted with the corresponding standard
677+
parameter in the subtype at the same position. This is because a standard parameter is more flexible
678+
than a positional-only parameter.
632679

633680
```py
634681
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
635682

636683
def standard_a(a: int) -> None: ...
637684
def positional_b(b: int, /) -> None: ...
638685

686+
# The names are not important in this context
639687
static_assert(is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[positional_b]))
640688

641689
def standard_float(a: float) -> None: ...
@@ -651,14 +699,33 @@ static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], Cal
651699
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
652700
```
653701

654-
And, with other kinds of parameter:
702+
The position of the positional-only parameters matter:
703+
704+
```py
705+
def multi_standard(a: float, b: int, c: str) -> None: ...
706+
def multi_positional1(b: int, c: bool, a: str, /) -> None: ...
707+
708+
# Here, the type of the parameter `a` makes the subtype relation invalid
709+
def multi_positional2(b: int, a: float, c: str, /) -> None: ...
710+
711+
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional1]))
712+
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional2]))
713+
```
714+
715+
#### Standard with variadic
716+
717+
A standard parameter in the supertype cannot be substituted with a variadic or keyword-variadic
718+
parameter in the subtype.
655719

656720
```py
657-
def variadic(*args: int) -> None: ...
658-
def keyword_variadic(**kwargs: int) -> None: ...
721+
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
722+
723+
def standard(a: int) -> None: ...
724+
def variadic(*a: int) -> None: ...
725+
def keyword_variadic(**a: int) -> None: ...
659726

660-
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[variadic]))
661-
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[keyword_variadic]))
727+
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[variadic]))
728+
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keyword_variadic]))
662729
```
663730

664731
#### Variadic
@@ -675,7 +742,7 @@ static_assert(is_subtype_of(CallableTypeFromFunction[variadic_float], CallableTy
675742
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[variadic_float]))
676743
```
677744

678-
A variadic parameter can be omitted in the subtype:
745+
The variadic parameter does not need to be present in the supertype:
679746

680747
```py
681748
def empty() -> None: ...
@@ -692,25 +759,31 @@ supertype should be checked against the variadic parameter.
692759
```py
693760
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
694761

695-
def variadic(*args: float) -> None: ...
696-
def positional_only(a: int, b: float, /) -> None: ...
697-
def positional_variadic(a: int, /, *args: int) -> None: ...
762+
def variadic(a: int, /, *args: float) -> None: ...
763+
764+
# Here, the parameter `b` and `c` are unmatched
765+
def positional_only(a: int, b: float, c: int, /) -> None: ...
766+
767+
# Here, the parameter `b` is unmatched and there's also a variadic parameter
768+
def positional_variadic(a: int, b: float, /, *args: int) -> None: ...
698769

699770
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_only]))
700771
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_variadic]))
701772
```
702773

703-
This is valid only for positional-only parameter, not any other parameter kind:
774+
This is valid only for positional-only parameters, not any other parameter kind:
704775

705776
```py
706-
def mixed(a: int, /, b: int) -> None: ...
777+
# Parameter 1 is matched with the one at the same position, parameter 2 is unmatched so uses the
778+
# variadic parameter but the standard parameter `c` remains and cannot be matched.
779+
def mixed(a: int, b: float, /, c: int) -> None: ...
707780

708781
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[mixed]))
709782
```
710783

711784
#### Keyword-only
712785

713-
For keyword-only parameters, the name matters:
786+
For keyword-only parameters, the name should be the same:
714787

715788
```py
716789
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
@@ -724,7 +797,7 @@ static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableT
724797
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_b]))
725798
```
726799

727-
But, the order of the keyword-only parameters does not:
800+
But, the order of the keyword-only parameters is not required to be the same:
728801

729802
```py
730803
def keyword_ab(*, a: float, b: float) -> None: ...
@@ -754,25 +827,27 @@ static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], Callable
754827
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
755828
```
756829

757-
Here, we mix keyword-only parameter with and without the default value:
830+
Keyword-only parameters with default values can be mixed with the ones without default values in any
831+
order:
758832

759833
```py
834+
# A keyword-only parameter with a default value follows the one without a default value (it's valid)
760835
def mixed(*, b: int = 1, a: int) -> None: ...
761836

762837
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[int_keyword]))
763838
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[mixed]))
764839
```
765840

766-
#### Keyword-only with positional
841+
#### Keyword-only with standard
767842

768843
```py
769844
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
770845

771846
def keywords1(*, a: int, b: int) -> None: ...
772847
def standard(b: float, a: float) -> None: ...
773848

774-
static_assert(is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keywords1]))
775849
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords1], CallableTypeFromFunction[standard]))
850+
static_assert(is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keywords1]))
776851
```
777852

778853
The subtype can include additional standard parameters as long as it has the default value:
@@ -781,8 +856,8 @@ The subtype can include additional standard parameters as long as it has the def
781856
def standard_with_default(b: float, a: float, c: float = 1) -> None: ...
782857
def standard_without_default(b: float, a: float, c: float) -> None: ...
783858

784-
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keywords1]))
785859
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_without_default], CallableTypeFromFunction[keywords1]))
860+
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keywords1]))
786861
```
787862

788863
Here, we mix keyword-only parameters with standard parameters:
@@ -791,16 +866,24 @@ Here, we mix keyword-only parameters with standard parameters:
791866
def keywords2(*, a: int, c: int, b: int) -> None: ...
792867
def mixed(b: float, a: float, *, c: float) -> None: ...
793868

794-
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[keywords2]))
795869
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords2], CallableTypeFromFunction[mixed]))
870+
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[keywords2]))
796871
```
797872

798873
But, we shouldn't consider any unmatched positional-only parameters:
799874

800875
```py
801-
def mixed_positional(b: float, /, a: float) -> None: ...
876+
def mixed_positional(b: float, /, a: float, *, c: float) -> None: ...
877+
878+
static_assert(not is_subtype_of(CallableTypeFromFunction[mixed_positional], CallableTypeFromFunction[keywords2]))
879+
```
880+
881+
But, an unmatched variadic parameter is still valid:
882+
883+
```py
884+
def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ...
802885

803-
static_assert(not is_subtype_of(CallableTypeFromFunction[mixed_positional], CallableTypeFromFunction[keywords1]))
886+
static_assert(is_subtype_of(CallableTypeFromFunction[mixed_variadic], CallableTypeFromFunction[keywords2]))
804887
```
805888

806889
#### Keyword-variadic
@@ -828,26 +911,30 @@ static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFro
828911

829912
#### Keyword-variadic with keyword-only
830913

831-
If the subtype has a variadic parameter then any unmatched positional-only parameter from the
832-
supertype should be checked against the variadic parameter.
914+
If the subtype has a keyword-variadic parameter then any unmatched keyword-only parameter from the
915+
supertype should be checked against the keyword-variadic parameter.
833916

834917
```py
835918
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
836919

837920
def kwargs(**kwargs: float) -> None: ...
838-
def keyword_only(*, a: int, b: float) -> None: ...
921+
def keyword_only(*, a: int, b: float, c: bool) -> None: ...
839922
def keyword_variadic(*, a: int, **kwargs: int) -> None: ...
840923

841924
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_only]))
842925
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_variadic]))
843926
```
844927

845-
This is valid only for positional-only parameter, not any other parameter kind:
928+
This is valid only for keyword-only parameters, not any other parameter kind:
846929

847930
```py
848-
def mixed(a: int, *, b: int) -> None: ...
931+
def mixed1(a: int, *, b: int) -> None: ...
932+
933+
# Same as above but with the default value
934+
def mixed2(a: int = 1, *, b: int) -> None: ...
849935

850-
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed]))
936+
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed1]))
937+
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed2]))
851938
```
852939

853940
#### Empty

crates/red_knot_python_semantic/src/types.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4826,9 +4826,8 @@ impl<'db> GeneralCallableType<'db> {
48264826
}
48274827
}
48284828

4829-
// At this point, the remaining parameters in `other` are keyword-only parameters or
4830-
// keyword variadic parameters. But, `self` could contain any unmatched positional
4831-
// parameters.
4829+
// At this point, the remaining parameters in `other` are keyword-only or keyword variadic.
4830+
// But, `self` could contain any unmatched positional parameters.
48324831
let (self_parameters, other_parameters) = parameters.into_remaining();
48334832

48344833
// Collect all the keyword-only parameters and the unmatched standard parameters.
@@ -4846,12 +4845,14 @@ impl<'db> GeneralCallableType<'db> {
48464845
ParameterKind::KeywordVariadic { .. } => {
48474846
self_keyword_variadic = self_parameter.annotated_type();
48484847
}
4849-
ParameterKind::PositionalOnly { .. } | ParameterKind::Variadic { .. } => {
4850-
// These are the unmatched parameters in `self` from the above loop. They
4851-
// cannot be matched against any parameter in `other` so the subtype relation
4852-
// is invalid.
4848+
ParameterKind::PositionalOnly { .. } => {
4849+
// These are the unmatched positional-only parameters in `self` from the
4850+
// previous loop. They cannot be matched against any parameter in `other` which
4851+
// only contains keyword-only and keyword-variadic parameters so the subtype
4852+
// relation is invalid.
48534853
return false;
48544854
}
4855+
ParameterKind::Variadic { .. } => {}
48554856
}
48564857
}
48574858

0 commit comments

Comments
 (0)