Repository: Unleash/unleash-client-python
Pull request #252 (2023-05-24 10:33:49)

docs(cache): fix typo in docstrings
# Description
Typo in `Cache` doctstrings

## Type of change
Documentation

# How Has This Been Tested?
N/a

# Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules



Patch:
diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py
index 33ee2c0..493e0cb 100644
--- a/UnleashClient/__init__.py
+++ b/UnleashClient/__init__.py
@@ -45,13 +45,14 @@ class UnleashClient:
 
     :param url: URL of the unleash server, required.
     :param app_name: Name of the application using the unleash client, required.
-    :param  environment: Name of the environment using the unleash client, optional & defaults to "default".
+    :param environment: Name of the environment using the unleash client, optional & defaults to "default".
     :param instance_id: Unique identifier for unleash client instance, optional & defaults to "unleash-client-python"
     :param refresh_interval: Provisioning refresh interval in seconds, optional & defaults to 15 seconds
     :param refresh_jitter: Provisioning refresh interval jitter in seconds, optional & defaults to None
     :param metrics_interval: Metrics refresh interval in seconds, optional & defaults to 60 seconds
     :param metrics_jitter: Metrics refresh interval jitter in seconds, optional & defaults to None
     :param disable_metrics: Disables sending metrics to unleash server, optional & defaults to false.
+    :param disable_registration: Disables registration with unleash server, optional & defaults to false.
     :param custom_headers: Default headers to send to unleash server, optional & defaults to empty.
     :param custom_options: Default requests parameters, optional & defaults to empty.  Can be used to skip SSL verification.
     :param custom_strategies: Dictionary of custom strategy names : custom strategy objects.
diff --git a/UnleashClient/cache.py b/UnleashClient/cache.py
index 4892964..b35df72 100644
--- a/UnleashClient/cache.py
+++ b/UnleashClient/cache.py
@@ -67,7 +67,7 @@ class FileCache(BaseCache):
         unleash_client = UnleashClient(
             "https://my.unleash.server.com",
             "HAMSTER_API",
-            cache=cache
+            cache=my_cache
         )
 
     :param name: Name of cache.
diff --git a/UnleashClient/features/Feature.py b/UnleashClient/features/Feature.py
index bd52287..968e0e2 100644
--- a/UnleashClient/features/Feature.py
+++ b/UnleashClient/features/Feature.py
@@ -1,5 +1,5 @@
 # pylint: disable=invalid-name
-from typing import Optional
+from typing import Dict, Optional, cast
 
 from UnleashClient.constants import DISABLED_VARIATION
 from UnleashClient.utils import LOGGER
@@ -36,6 +36,8 @@ class Feature:
         # Stats tracking
         self.yes_count = 0
         self.no_count = 0
+        ## { [ variant name ]: number }
+        self.variant_counts: Dict[str, int] = {}
 
     def reset_stats(self) -> None:
         """
@@ -45,6 +47,7 @@ class Feature:
         """
         self.yes_count = 0
         self.no_count = 0
+        self.variant_counts = {}
 
     def increment_stats(self, result: bool) -> None:
         """
@@ -58,6 +61,15 @@ class Feature:
         else:
             self.no_count += 1
 
+    def _count_variant(self, variant_name: str) -> None:
+        """
+        Count a specific variant.
+
+        :param variant_name: The name of the variant to count.
+        :return:
+        """
+        self.variant_counts[variant_name] = self.variant_counts.get(variant_name, 0) + 1
+
     def is_enabled(
         self, context: dict = None, default_value: bool = False
     ) -> bool:  # pylint: disable=unused-argument
@@ -105,4 +117,5 @@ class Feature:
             except Exception as variant_exception:
                 LOGGER.warning("Error selecting variant: %s", variant_exception)
 
+        self._count_variant(cast(str, variant["name"]))
         return variant
diff --git a/UnleashClient/periodic_tasks/send_metrics.py b/UnleashClient/periodic_tasks/send_metrics.py
index 9403bce..5734dc2 100644
--- a/UnleashClient/periodic_tasks/send_metrics.py
+++ b/UnleashClient/periodic_tasks/send_metrics.py
@@ -26,6 +26,7 @@ def aggregate_and_send_metrics(
             features[feature_name].name: {
                 "yes": features[feature_name].yes_count,
                 "no": features[feature_name].no_count,
+                "variants": features[feature_name].variant_counts,
             }
         }
 


Test patch:
diff --git a/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py b/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py
index 979fd49..9bf223f 100644
--- a/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py
+++ b/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta, timezone
 
 import responses
 
+from tests.utilities.mocks.mock_variants import VARIANTS
 from tests.utilities.testing_constants import (
     APP_NAME,
     CUSTOM_HEADERS,
@@ -16,6 +17,7 @@ from UnleashClient.constants import METRIC_LAST_SENT_TIME, METRICS_URL
 from UnleashClient.features import Feature
 from UnleashClient.periodic_tasks import aggregate_and_send_metrics
 from UnleashClient.strategies import Default, RemoteAddress
+from UnleashClient.variants import Variants
 
 FULL_METRICS_URL = URL + METRICS_URL
 print(FULL_METRICS_URL)
@@ -33,10 +35,19 @@ def test_aggregate_and_send_metrics():
     my_feature1.yes_count = 1
     my_feature1.no_count = 1
 
-    my_feature2 = Feature("My Feature2", True, strategies)
+    my_feature2 = Feature(
+        "My Feature2", True, strategies, variants=Variants(VARIANTS, "My Feature2")
+    )
     my_feature2.yes_count = 2
     my_feature2.no_count = 2
 
+    feature2_variant_counts = {
+        "VarA": 56,
+        "VarB": 0,
+        "VarC": 4,
+    }
+    my_feature2.variant_counts = feature2_variant_counts
+
     my_feature3 = Feature("My Feature3", True, strategies)
     my_feature3.yes_count = 0
     my_feature3.no_count = 0
@@ -53,6 +64,10 @@ def test_aggregate_and_send_metrics():
     assert len(request["bucket"]["toggles"].keys()) == 2
     assert request["bucket"]["toggles"]["My Feature1"]["yes"] == 1
     assert request["bucket"]["toggles"]["My Feature1"]["no"] == 1
+    assert (
+        request["bucket"]["toggles"]["My Feature2"]["variants"]
+        == feature2_variant_counts
+    )
     assert "My Feature3" not in request["bucket"]["toggles"].keys()
     assert cache.get(METRIC_LAST_SENT_TIME) > start_time
 
diff --git a/tests/unit_tests/test_features.py b/tests/unit_tests/test_features.py
index 6c44e75..5e0160d 100644
--- a/tests/unit_tests/test_features.py
+++ b/tests/unit_tests/test_features.py
@@ -76,3 +76,31 @@ def test_select_variation_variation(test_feature_variants):
     selected_variant = test_feature_variants.get_variant({"userId": "2"})
     assert selected_variant["enabled"]
     assert selected_variant["name"] == "VarB"
+
+
+def test_variant_metrics_are_reset(test_feature_variants):
+    test_feature_variants.get_variant({"userId": "2"})
+    assert test_feature_variants.variant_counts["VarB"] == 1
+
+    test_feature_variants.reset_stats()
+    assert not test_feature_variants.variant_counts
+
+
+def test_variant_metrics_with_existing_variant(test_feature_variants):
+    for iteration in range(1, 7):
+        test_feature_variants.get_variant({"userId": "2"})
+        assert test_feature_variants.variant_counts["VarB"] == iteration
+
+
+def test_variant_metrics_with_disabled_feature(test_feature_variants):
+    test_feature_variants.enabled = False
+    assert not test_feature_variants.is_enabled()
+    for iteration in range(1, 7):
+        test_feature_variants.get_variant({})
+        assert test_feature_variants.variant_counts["disabled"] == iteration
+
+
+def test_variant_metrics_feature_has_no_variants(test_feature):
+    for iteration in range(1, 7):
+        test_feature.get_variant({})
+        assert test_feature.variant_counts["disabled"] == iteration