Repository: stravalib/stravalib (python)

Issue:
BUG: ActivityType comparison and serialization issues
### Stravalib version checks

- [X] I have tested this in a new clean environment with only stravalib and core python files.

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the [latest version](https://pypi.org/project/stravalib/) of stravalib.

- [X] I have confirmed this bug exists on the [main branch](https://github.com/stravalib/stravalib) of stravalib.


### What operating system are you seeing the problem on?

Mac

### What version of python or you running?

```python
3.10.10
```


### Reproducible Example

```python
from stravalib.model import SummaryActivity

a = SummaryActivity.model_validate({'type': 'Run'})
a.type == 'Run'
# Expecting True, but returns False
```


### Issue Description

An activity's `type` attribute is of type `RelaxedActivityType`, but (since the pydantic v2 bump) doesn't behave as a `Literal[str]` anymore. This leads to subtle but potentially dangerous bugs, as seen above. This is a breaking change in 2.0 that we didn't document.

### Expected Behavior

`SummaryActivity.type` should return a type comparable with the previously used `Literal[]`, i.e., something that can be compared to `str` types and should be serialized as `str`.

### Your environment

Not appliccable

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

Pull request:
FIX: ensures ActivityType instances can be compared to str
closes #583

## Description

Ensures ActivityType instances can be compared to str. This prevents an undocumented breaking change from stravalib v1.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Does your PR include tests

- [x] Yes

Updated existing test.

## Did you include your contribution to the change log?

- [x] Yes, `changelog.md` is up-to-date.


Patch:
diff --git a/changelog.md b/changelog.md
index 65d7184..2681c92 100644
--- a/changelog.md
+++ b/changelog.md
@@ -9,6 +9,7 @@
 ### Fixed
 
 - Some override moved from DetailedActivity to SummaryActivity (@enadeau, #570)
+- Ensures ActivityType instances can be compared to str (@jsamoocha, #583)
 
 ## v2.0.0
 
diff --git a/src/stravalib/model.py b/src/stravalib/model.py
index 8bf4b38..68938cd 100644
--- a/src/stravalib/model.py
+++ b/src/stravalib/model.py
@@ -522,6 +522,13 @@ class RelaxedActivityType(ActivityType):
             values = "Workout"
         return values
 
+    def __eq__(self, other: Any) -> bool:
+        if isinstance(other, RelaxedActivityType):
+            return other.root == self.root
+        elif isinstance(other, str):
+            return other == self.root
+        return False
+
 
 class RelaxedSportType(SportType):
     """A class that extends the list of Literal values allowed for Sport Types
@@ -552,6 +559,13 @@ class RelaxedSportType(SportType):
             values = "Workout"
         return values
 
+    def __eq__(self, other: Any) -> bool:
+        if isinstance(other, RelaxedSportType):
+            return other.root == self.root
+        elif isinstance(other, str):
+            return other == self.root
+        return False
+
 
 class LatLon(LatLng):
     """Stores lat / lon values or None."""


Test patch:
diff --git a/src/stravalib/tests/unit/test_model.py b/src/stravalib/tests/unit/test_model.py
index 5509f94..5914d1e 100644
--- a/src/stravalib/tests/unit/test_model.py
+++ b/src/stravalib/tests/unit/test_model.py
@@ -17,14 +17,13 @@ from stravalib.model import (
     Duration,
     Lap,
     LatLon,
-    RelaxedActivityType,
-    RelaxedSportType,
     Route,
     Segment,
     SegmentEffort,
     SegmentExplorerResult,
     Split,
     SubscriptionCallback,
+    SummaryActivity,
     SummarySegmentEffort,
     Timezone,
     Velocity,
@@ -145,7 +144,6 @@ def test_subscription_callback_field_names():
     assert sub_callback.hub_verify_token == "STRAVA"
 
 
-# TODO: do we want to continue to support type?
 @pytest.mark.parametrize(
     "klass,attr,given_type,expected_type",
     (
@@ -160,11 +158,27 @@ def test_subscription_callback_field_names():
 def test_relaxed_activity_type_validation(
     klass, attr, given_type, expected_type
 ):
-    obj = getattr(klass(**{attr: given_type}), attr)
-    if attr == "sport_type":
-        assert obj == RelaxedSportType(root=expected_type)
-    else:
-        assert obj == RelaxedActivityType(root=expected_type)
+    obj = klass.model_validate({attr: given_type})
+    assert getattr(obj, attr) == expected_type
+
+
+@pytest.mark.parametrize(
+    "a_attr,a_value,b_attr,b_value,expected_attr_equality",
+    (
+        ("type", "Run", "type", "Run", True),
+        ("type", "Run", "type", "Ride", False),
+        ("type", "Run", "id", 42, False),
+        ("sport_type", "Run", "sport_type", "Run", True),
+        ("sport_type", "Run", "sport_type", "Ride", False),
+        ("sport_type", "Run", "id", 42, False),
+    ),
+)
+def test_relaxed_activity_type_equality(
+    a_attr, a_value, b_attr, b_value, expected_attr_equality
+):
+    a = SummaryActivity.model_validate({a_attr: a_value})
+    b = SummaryActivity.model_validate({b_attr: b_value})
+    assert (getattr(a, a_attr) == getattr(b, b_attr)) == expected_attr_equality
 
 
 @pytest.mark.parametrize(