Repository: pydantic/pydantic

Issue:
Invalid `is` check in credit card validation
https://github.com/samuelcolvin/pydantic/blob/master/pydantic/types.py#L669

```python
if card_number.brand is (PaymentCardBrand.visa or PaymentCardBrand.mastercard)
```
Assuming brand was reduced to the integers, and we had 3 brands, then this is the equivalent of:
`3 is (1 or 2)`, which you'd hope were false, `1 is (1 or 2)` which you'd hope were true, and `2 is (1 or 2)` which you'd also hope were true. However, `or` short-circuits, so these reduce to `3 is 1`, `1 is 1`, and `2 is 1`. This means the preferred outcome for the first two cases works, but it would fail for mastercard.

Perhaps this should be
```python
if card_number.brand is PaymentCardBrand.visa or card_number.brand is PaymentCardBrand.mastercard:
```

I think this was added relatively recently - https://github.com/samuelcolvin/pydantic/commit/0c5793770be1de8b1a28c3fbaca669cf3bab1e34#diff-470d0730db2e82eaaf27ad3655a667b3R582 - so hopefully it hasn't affected too many people.


Comments:
Probably works because of some `Enum` class magic, but could be reworked to:
```py
if card_number.brand in (PaymentCardBrand.visa, PaymentCardBrand.mastercard)
```
I guess it depends if equality is acceptable compared to identity?
I don't think magic works with is, and I'm kinda glad of that:
```
>>> from enum import Enum
>>> class CardType(Enum):
...     Diners = 1
...     Visa = 2
...     MasterCard = 3
...
>>> CardType.Visa is (CardType.MasterCard or CardType.Visa)
False
```
Good to confirm.
Assuming this is resolved. 
Sadly not resolved, code is still
```python
if card_number.brand is (PaymentCardBrand.visa or PaymentCardBrand.mastercard):
```

which won't work if `card_number.brand` is `PaymentCardBrand.mastercard`.
Yes, I was just confirming the logic is broken. @mike-hart you might want to submit a PR referencing this issue with the fix.
The use of `x is (y or z)` is just a mistake, it simplifies to `x is y` if y is truey or `x is z` otherwise. In this case since `PaymentCardBrand.visa` is truey we're effectively just doing `brand is PaymentCardBrand.visa`.

I'll replace it with an `in` check.

A bit more background on enums:
* Identity checks work with the actual enum member but not with the enum value
* equality checks work with both the enum member and the enum value, but **only if** the enum inherits from the correct type as well as `Enum`.

```py
class PaymentCardBrand(str, Enum):
    amex = 'American Express'
    mastercard = 'Mastercard'
    visa = 'Visa'
    other = 'other'

b = PaymentCardBrand.visa
b is PaymentCardBrand.visa
#> True
b == PaymentCardBrand.visa
#> True

b = 'Visa'
b is PaymentCardBrand.visa
#> False
b == PaymentCardBrand.visa
#> True (This would not work with the current PaymentCardBrand which doesn't inherit from str)
```

I don't know if `card_number.brand` can be a string or only a `PaymentCardBrand`, but better to fix anyway.

Patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -591,7 +591,7 @@ def get_secret_value(self) -> bytes:
         return self._secret_value
 
 
-class PaymentCardBrand(Enum):
+class PaymentCardBrand(str, Enum):
     amex = 'American Express'
     mastercard = 'Mastercard'
     visa = 'Visa'
@@ -666,7 +666,7 @@ def validate_length_for_brand(cls, card_number: 'PaymentCardNumber') -> 'Payment
         https://en.wikipedia.org/wiki/Payment_card_number#Issuer_identification_number_(IIN)
         """
         required_length: Optional[int] = None
-        if card_number.brand is (PaymentCardBrand.visa or PaymentCardBrand.mastercard):
+        if card_number.brand in {PaymentCardBrand.visa, PaymentCardBrand.mastercard}:
             required_length = 16
             valid = len(card_number) == required_length
         elif card_number.brand is PaymentCardBrand.amex:


Test patch:
diff --git a/tests/test_types_payment_card_number.py b/tests/test_types_payment_card_number.py
--- a/tests/test_types_payment_card_number.py
+++ b/tests/test_types_payment_card_number.py
@@ -78,6 +78,7 @@ def test_validate_luhn_check_digit(card_number: str, valid: bool):
         (VALID_AMEX, PaymentCardBrand.amex, True),
         (VALID_OTHER, PaymentCardBrand.other, True),
         (LEN_INVALID, PaymentCardBrand.visa, False),
+        (VALID_AMEX, PaymentCardBrand.mastercard, False),
     ],
 )
 def test_length_for_brand(card_number: str, brand: PaymentCardBrand, valid: bool):
@@ -123,3 +124,21 @@ def test_error_types(card_number: Any, error_message: str):
     with pytest.raises(ValidationError, match=error_message) as exc_info:
         PaymentCard(card_number=card_number)
     assert exc_info.value.json().startswith('[')
+
+
+def test_payment_card_brand():
+    b = PaymentCardBrand.visa
+    assert str(b) == 'Visa'
+    assert b is PaymentCardBrand.visa
+    assert b == PaymentCardBrand.visa
+    assert b in {PaymentCardBrand.visa, PaymentCardBrand.mastercard}
+
+    b = 'Visa'
+    assert b is not PaymentCardBrand.visa
+    assert b == PaymentCardBrand.visa
+    assert b in {PaymentCardBrand.visa, PaymentCardBrand.mastercard}
+
+    b = PaymentCardBrand.amex
+    assert b is not PaymentCardBrand.visa
+    assert b != PaymentCardBrand.visa
+    assert b not in {PaymentCardBrand.visa, PaymentCardBrand.mastercard}