From 808493029b958c822b4487ccfdcad07cfde56ce2 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 6 Aug 2024 09:50:52 -0700 Subject: [PATCH 1/5] disallow any generics --- featuremanagement/_defaultfilters.py | 10 +++---- featuremanagement/_featurefilters.py | 4 +-- featuremanagement/_featuremanager.py | 9 ++++-- featuremanagement/_featuremanagerbase.py | 11 ++++--- featuremanagement/_models/_allocation.py | 30 +++++++++++++------ .../_models/_feature_conditions.py | 6 ++-- featuremanagement/_models/_feature_flag.py | 4 +-- featuremanagement/_models/_telemetry.py | 3 +- .../_models/_variant_reference.py | 4 +-- featuremanagement/aio/_defaultfilters.py | 4 +-- featuremanagement/aio/_featurefilters.py | 4 +-- featuremanagement/aio/_featuremanager.py | 7 +++-- mypy.ini | 2 +- 13 files changed, 58 insertions(+), 40 deletions(-) diff --git a/featuremanagement/_defaultfilters.py b/featuremanagement/_defaultfilters.py index ead5c6a..99bef41 100644 --- a/featuremanagement/_defaultfilters.py +++ b/featuremanagement/_defaultfilters.py @@ -44,7 +44,7 @@ class TimeWindowFilter(FeatureFilter): Feature Filter that determines if the current time is within the time window. """ - def evaluate(self, context: Mapping, **kwargs: Dict[str, Any]) -> bool: + def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -87,7 +87,7 @@ def _is_targeted(context_id: str, rollout_percentage: int) -> bool: return percentage < rollout_percentage def _target_group( - self, target_user: Optional[str], target_group: str, group: Mapping, feature_flag_name: str + self, target_user: Optional[str], target_group: str, group: Mapping[str, Any], feature_flag_name: str ) -> bool: group_rollout_percentage = group.get(ROLLOUT_PERCENTAGE_KEY, 0) if not target_user: @@ -96,7 +96,7 @@ def _target_group( return self._is_targeted(audience_context_id, group_rollout_percentage) - def evaluate(self, context: Mapping, **kwargs: Dict[str, Any]) -> bool: + def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -156,11 +156,11 @@ def evaluate(self, context: Mapping, **kwargs: Dict[str, Any]) -> bool: return self._is_targeted(context_id, default_rollout_percentage) @staticmethod - def _validate(groups: List, default_rollout_percentage: int) -> None: + def _validate(groups: List[Dict[str, Any]], default_rollout_percentage: int) -> None: # Validate the audience settings if default_rollout_percentage < 0 or default_rollout_percentage > 100: raise TargetingException("DefaultRolloutPercentage must be between 0 and 100") for group in groups: - if group.get(ROLLOUT_PERCENTAGE_KEY) < 0 or group.get(ROLLOUT_PERCENTAGE_KEY) > 100: + if group.get(ROLLOUT_PERCENTAGE_KEY) < 0 or group.get(ROLLOUT_PERCENTAGE_KEY) > 100: # type: ignore raise TargetingException("RolloutPercentage must be between 0 and 100") diff --git a/featuremanagement/_featurefilters.py b/featuremanagement/_featurefilters.py index 3cd3d8b..0f19545 100644 --- a/featuremanagement/_featurefilters.py +++ b/featuremanagement/_featurefilters.py @@ -13,7 +13,7 @@ class FeatureFilter(ABC): """ @abstractmethod - def evaluate(self, context: Mapping, **kwargs: Dict[str, Any]) -> bool: + def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -33,7 +33,7 @@ def name(self) -> str: return self.__class__.__name__ @staticmethod - def alias(alias: str) -> Callable: + def alias(alias: str) -> Callable[..., Any]: """ Decorator to set the alias for the filter. diff --git a/featuremanagement/_featuremanager.py b/featuremanagement/_featuremanager.py index 003cbd0..63747f3 100644 --- a/featuremanagement/_featuremanager.py +++ b/featuremanagement/_featuremanager.py @@ -28,9 +28,12 @@ class FeatureManager(FeatureManagerBase): evaluated. """ - def __init__(self, configuration: Mapping, **kwargs: Dict[str, Any]): + def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): super().__init__(configuration, **kwargs) - filters = [TimeWindowFilter(), TargetingFilter()] + cast(List, kwargs.pop(PROVIDED_FEATURE_FILTERS, [])) + self._filters: Dict[str, FeatureFilter] = {} + filters = [TimeWindowFilter(), TargetingFilter()] + cast( + List[FeatureFilter], kwargs.pop(PROVIDED_FEATURE_FILTERS, []) + ) for feature_filter in filters: if not isinstance(feature_filter, FeatureFilter): @@ -105,7 +108,7 @@ def get_variant( # type: ignore return result.variant def _check_feature_filters( - self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Dict + self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Dict[str, Any] ) -> None: feature_flag = evaluation_event.feature if not feature_flag: diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index b282713..8b330eb 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -21,7 +21,7 @@ FEATURE_FILTER_PARAMETERS = "parameters" -def _get_feature_flag(configuration: Mapping, feature_flag_name: str) -> Optional[FeatureFlag]: +def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) -> Optional[FeatureFlag]: """ Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object. @@ -44,7 +44,7 @@ def _get_feature_flag(configuration: Mapping, feature_flag_name: str) -> Optiona return None -def _list_feature_flag_names(configuration: Mapping) -> List[str]: +def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: """ List of all feature flag names. @@ -70,12 +70,11 @@ class FeatureManagerBase(ABC): Base class for Feature Manager. This class is responsible for all shared logic between the sync and async. """ - def __init__(self, configuration: Mapping, **kwargs: Dict[str, Any]): - self._filters: Dict = {} + def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): if configuration is None or not isinstance(configuration, Mapping): raise AttributeError("Configuration must be a non-empty dictionary") self._configuration = configuration - self._cache: Dict = {} + self._cache: Dict[str, Optional[FeatureFlag]] = {} self._copy = configuration.get(FEATURE_MANAGEMENT_KEY) self._on_feature_evaluated = kwargs.pop("on_feature_evaluated", None) @@ -214,7 +213,7 @@ def _variant_name_to_variant(self, feature_flag: FeatureFlag, variant_name: Opti for variant_reference in feature_flag.variants: if variant_reference.name == variant_name: configuration = variant_reference.configuration_value - if not configuration: + if not configuration and variant_reference.configuration_reference: configuration = self._configuration.get(variant_reference.configuration_reference) return Variant(variant_reference.name, configuration) return None diff --git a/featuremanagement/_models/_allocation.py b/featuremanagement/_models/_allocation.py index 122b1a9..8ea5304 100644 --- a/featuremanagement/_models/_allocation.py +++ b/featuremanagement/_models/_allocation.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import cast, List, Optional, Mapping, Dict, Any +from typing import cast, List, Optional, Mapping, Dict, Any, Union from dataclasses import dataclass from typing_extensions import Self from ._constants import DEFAULT_WHEN_ENABLED, DEFAULT_WHEN_DISABLED, USER, GROUP, PERCENTILE, SEED @@ -16,7 +16,7 @@ class UserAllocation: """ variant: str - users: list + users: List[str] @dataclass @@ -26,7 +26,7 @@ class GroupAllocation: """ variant: str - groups: list + groups: List[str] class PercentileAllocation: @@ -35,12 +35,12 @@ class PercentileAllocation: """ def __init__(self) -> None: - self._variant = None + self._variant: Optional[str] = None self._percentile_from: int = 0 self._percentile_to: Optional[int] = None @classmethod - def convert_from_json(cls, json: Mapping) -> Self: + def convert_from_json(cls, json: Mapping[str, Union[str, int]]) -> Self: """ Convert a JSON object to PercentileAllocation. @@ -51,9 +51,21 @@ def convert_from_json(cls, json: Mapping) -> Self: if not json: raise ValueError("Percentile allocation is not valid.") user_allocation = cls() - user_allocation._variant = json.get("variant") - user_allocation._percentile_from = json.get("from", 0) - user_allocation._percentile_to = json.get("to") + + variant = json.get("variant") + if not variant or not isinstance(variant, str): + raise ValueError("Percentile allocation does not have a valid assigned variant.") + user_allocation._variant = variant + + percentile_from = json.get("from", 0) + if not isinstance(percentile_from, int): + raise ValueError("Percentile allocation does not have a valid starting percentile.") + user_allocation._percentile_from = percentile_from + + percentile_to = json.get("to") + if not percentile_to or not isinstance(percentile_to, int): + raise ValueError("Percentile allocation does not have a valid ending percentile.") + user_allocation._percentile_to = percentile_to return user_allocation @property @@ -101,7 +113,7 @@ def __init__(self, feature_name: str) -> None: self._seed = "allocation\n" + feature_name @classmethod - def convert_from_json(cls, json: Dict, feature_name: str) -> Optional[Self]: + def convert_from_json(cls, json: Dict[str, Any], feature_name: str) -> Optional[Self]: """ Convert a JSON object to Allocation. diff --git a/featuremanagement/_models/_feature_conditions.py b/featuremanagement/_models/_feature_conditions.py index 8e28737..0d811d8 100644 --- a/featuremanagement/_models/_feature_conditions.py +++ b/featuremanagement/_models/_feature_conditions.py @@ -4,7 +4,7 @@ # license information. # ------------------------------------------------------------------------- from collections.abc import Mapping -from typing import List +from typing import Any, Dict, List from typing_extensions import Self from ._constants import ( FEATURE_FLAG_CLIENT_FILTERS, @@ -22,7 +22,7 @@ class FeatureConditions: def __init__(self) -> None: self._requirement_type = REQUIREMENT_TYPE_ANY - self._client_filters: List[dict] = [] + self._client_filters: List[Dict[str, Any]] = [] @classmethod def convert_from_json(cls, feature_name: str, json_value: str) -> Self: @@ -55,7 +55,7 @@ def requirement_type(self) -> str: return self._requirement_type @property - def client_filters(self) -> List[dict]: + def client_filters(self) -> List[Dict[str, Any]]: """ Get the client filters for the feature flag. diff --git a/featuremanagement/_models/_feature_flag.py b/featuremanagement/_models/_feature_flag.py index 2ee314b..98cfd8e 100644 --- a/featuremanagement/_models/_feature_flag.py +++ b/featuremanagement/_models/_feature_flag.py @@ -32,7 +32,7 @@ def __init__(self) -> None: self._telemetry: Telemetry = Telemetry() @classmethod - def convert_from_json(cls, json_value: Mapping) -> Self: + def convert_from_json(cls, json_value: Mapping[str, Any]) -> Self: """ Convert a JSON object to FeatureFlag. @@ -56,7 +56,7 @@ def convert_from_json(cls, json_value: Mapping) -> Self: json_value.get(FEATURE_FLAG_ALLOCATION, None), feature_flag._id ) if FEATURE_FLAG_VARIANTS in json_value: - variants: List[Mapping] = json_value.get(FEATURE_FLAG_VARIANTS, []) + variants: List[Mapping[str, Any]] = json_value.get(FEATURE_FLAG_VARIANTS, []) feature_flag._variants = [] for variant in variants: if variant: diff --git a/featuremanagement/_models/_telemetry.py b/featuremanagement/_models/_telemetry.py index 55e3917..c54ad84 100644 --- a/featuremanagement/_models/_telemetry.py +++ b/featuremanagement/_models/_telemetry.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- +from typing import Dict from dataclasses import dataclass, field @@ -13,4 +14,4 @@ class Telemetry: """ enabled: bool = False - metadata: dict = field(default_factory=dict) + metadata: Dict[str, str] = field(default_factory=dict) diff --git a/featuremanagement/_models/_variant_reference.py b/featuremanagement/_models/_variant_reference.py index 7d6b059..afcab33 100644 --- a/featuremanagement/_models/_variant_reference.py +++ b/featuremanagement/_models/_variant_reference.py @@ -4,7 +4,7 @@ # license information. # ------------------------------------------------------------------------- from dataclasses import dataclass -from typing import Optional, Mapping +from typing import Optional, Mapping, Any from typing_extensions import Self from ._constants import VARIANT_REFERENCE_NAME, CONFIGURATION_VALUE, CONFIGURATION_REFERENCE, STATUS_OVERRIDE @@ -22,7 +22,7 @@ def __init__(self) -> None: self._status_override = None @classmethod - def convert_from_json(cls, json: Mapping) -> Self: + def convert_from_json(cls, json: Mapping[str, Any]) -> Self: """ Convert a JSON object to VariantReference. diff --git a/featuremanagement/aio/_defaultfilters.py b/featuremanagement/aio/_defaultfilters.py index 336721c..2d6e5b8 100644 --- a/featuremanagement/aio/_defaultfilters.py +++ b/featuremanagement/aio/_defaultfilters.py @@ -20,7 +20,7 @@ class TimeWindowFilter(FeatureFilter): def __init__(self) -> None: self._filter = SyncTimeWindowFilter() - async def evaluate(self, context: Mapping, **kwargs: Dict[str, Any]) -> bool: + async def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -40,7 +40,7 @@ class TargetingFilter(FeatureFilter): def __init__(self) -> None: self._filter = SyncTargetingFilter() - async def evaluate(self, context: Mapping[str, Any], **kwargs: Dict[str, Any]) -> bool: + async def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: """ Determine if the feature flag is enabled for the given context. diff --git a/featuremanagement/aio/_featurefilters.py b/featuremanagement/aio/_featurefilters.py index 635014e..986e3ea 100644 --- a/featuremanagement/aio/_featurefilters.py +++ b/featuremanagement/aio/_featurefilters.py @@ -13,7 +13,7 @@ class FeatureFilter(ABC): """ @abstractmethod - async def evaluate(self, context: Mapping, **kwargs: Dict[str, Any]) -> bool: + async def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -33,7 +33,7 @@ def name(self) -> str: return self.__class__.__name__ @staticmethod - def alias(alias: str) -> Callable: + def alias(alias: str) -> Callable[..., Any]: """ Decorator to set the alias for the filter. diff --git a/featuremanagement/aio/_featuremanager.py b/featuremanagement/aio/_featuremanager.py index 75d6eae..968c254 100644 --- a/featuremanagement/aio/_featuremanager.py +++ b/featuremanagement/aio/_featuremanager.py @@ -29,9 +29,12 @@ class FeatureManager(FeatureManagerBase): evaluated. """ - def __init__(self, configuration: Mapping, **kwargs: Dict[str, Any]): + def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): super().__init__(configuration, **kwargs) - filters = [TimeWindowFilter(), TargetingFilter()] + cast(List, kwargs.pop(PROVIDED_FEATURE_FILTERS, [])) + self._filters: Dict[str, FeatureFilter] = {} + filters = [TimeWindowFilter(), TargetingFilter()] + cast( + List[FeatureFilter], kwargs.pop(PROVIDED_FEATURE_FILTERS, []) + ) for feature_filter in filters: if not isinstance(feature_filter, FeatureFilter): diff --git a/mypy.ini b/mypy.ini index 1f48376..9cc9d19 100644 --- a/mypy.ini +++ b/mypy.ini @@ -16,7 +16,7 @@ check_untyped_defs = True # get passing if you use a lot of untyped libraries disallow_subclassing_any = True disallow_untyped_decorators = True -disallow_any_generics = False +disallow_any_generics = True # These next few are various gradations of forcing use of type annotations disallow_untyped_calls = False From 283a2158de0321a4d1733b8bade13bf7cd440251 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 6 Aug 2024 10:27:11 -0700 Subject: [PATCH 2/5] Fixing a few type: ignore --- featuremanagement/_defaultfilters.py | 6 +++--- featuremanagement/_featurefilters.py | 2 +- featuremanagement/_featuremanager.py | 20 +++++++++----------- featuremanagement/_featuremanagerbase.py | 2 +- featuremanagement/aio/_defaultfilters.py | 4 ++-- featuremanagement/aio/_featurefilters.py | 2 +- featuremanagement/aio/_featuremanager.py | 20 +++++++++----------- mypy.ini | 4 ++-- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/featuremanagement/_defaultfilters.py b/featuremanagement/_defaultfilters.py index 99bef41..cffc0bd 100644 --- a/featuremanagement/_defaultfilters.py +++ b/featuremanagement/_defaultfilters.py @@ -44,7 +44,7 @@ class TimeWindowFilter(FeatureFilter): Feature Filter that determines if the current time is within the time window. """ - def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: + def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -96,7 +96,7 @@ def _target_group( return self._is_targeted(audience_context_id, group_rollout_percentage) - def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: + def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -162,5 +162,5 @@ def _validate(groups: List[Dict[str, Any]], default_rollout_percentage: int) -> raise TargetingException("DefaultRolloutPercentage must be between 0 and 100") for group in groups: - if group.get(ROLLOUT_PERCENTAGE_KEY) < 0 or group.get(ROLLOUT_PERCENTAGE_KEY) > 100: # type: ignore + if group.get(ROLLOUT_PERCENTAGE_KEY, 0) < 0 or group.get(ROLLOUT_PERCENTAGE_KEY, 100) > 100: raise TargetingException("RolloutPercentage must be between 0 and 100") diff --git a/featuremanagement/_featurefilters.py b/featuremanagement/_featurefilters.py index 0f19545..5f09ab1 100644 --- a/featuremanagement/_featurefilters.py +++ b/featuremanagement/_featurefilters.py @@ -13,7 +13,7 @@ class FeatureFilter(ABC): """ @abstractmethod - def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: + def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. diff --git a/featuremanagement/_featuremanager.py b/featuremanagement/_featuremanager.py index 63747f3..80ba5ba 100644 --- a/featuremanagement/_featuremanager.py +++ b/featuremanagement/_featuremanager.py @@ -28,7 +28,7 @@ class FeatureManager(FeatureManagerBase): evaluated. """ - def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): + def __init__(self, configuration: Mapping[str, Any], **kwargs: Any): super().__init__(configuration, **kwargs) self._filters: Dict[str, FeatureFilter] = {} filters = [TimeWindowFilter(), TargetingFilter()] + cast( @@ -41,7 +41,7 @@ def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): self._filters[feature_filter.name] = feature_filter @overload # type: ignore - def is_enabled(self, feature_flag_id: str, user_id: str, **kwargs: Dict[str, Any]) -> bool: + def is_enabled(self, feature_flag_id: str, user_id: str, **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -51,7 +51,7 @@ def is_enabled(self, feature_flag_id: str, user_id: str, **kwargs: Dict[str, Any :rtype: bool """ - def is_enabled(self, feature_flag_id: str, *args: Any, **kwargs: Dict[str, Any]) -> bool: # type: ignore + def is_enabled(self, feature_flag_id: str, *args: Any, **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -73,7 +73,7 @@ def is_enabled(self, feature_flag_id: str, *args: Any, **kwargs: Dict[str, Any]) return result.enabled @overload # type: ignore - def get_variant(self, feature_flag_id: str, user_id: str, **kwargs: Dict[str, Any]) -> Optional[Variant]: + def get_variant(self, feature_flag_id: str, user_id: str, **kwargs: Any) -> Optional[Variant]: """ Determine the variant for the given context. @@ -83,9 +83,7 @@ def get_variant(self, feature_flag_id: str, user_id: str, **kwargs: Dict[str, An :rtype: Variant """ - def get_variant( # type: ignore - self, feature_flag_id: str, *args: Any, **kwargs: Dict[str, Any] - ) -> Optional[Variant]: + def get_variant(self, feature_flag_id: str, *args: Any, **kwargs: Any) -> Optional[Variant]: """ Determine the variant for the given context. @@ -108,7 +106,7 @@ def get_variant( # type: ignore return result.variant def _check_feature_filters( - self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Dict[str, Any] + self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Any ) -> None: feature_flag = evaluation_event.feature if not feature_flag: @@ -126,8 +124,8 @@ def _check_feature_filters( for feature_filter in feature_filters: filter_name = feature_filter[FEATURE_FILTER_NAME] - kwargs["user"] = targeting_context.user_id # type: ignore - kwargs["groups"] = targeting_context.groups # type: ignore + kwargs["user"] = targeting_context.user_id + kwargs["groups"] = targeting_context.groups if filter_name not in self._filters: raise ValueError(f"Feature flag {feature_flag.name} has unknown filter {filter_name}") if feature_conditions.requirement_type == REQUIREMENT_TYPE_ALL: @@ -139,7 +137,7 @@ def _check_feature_filters( break def _check_feature( - self, feature_flag_id: str, targeting_context: TargetingContext, **kwargs: Dict[str, Any] + self, feature_flag_id: str, targeting_context: TargetingContext, **kwargs: Any ) -> EvaluationEvent: """ Determine if the feature flag is enabled for the given context. diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 8b330eb..3c65d93 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -70,7 +70,7 @@ class FeatureManagerBase(ABC): Base class for Feature Manager. This class is responsible for all shared logic between the sync and async. """ - def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): + def __init__(self, configuration: Mapping[str, Any], **kwargs: Any): if configuration is None or not isinstance(configuration, Mapping): raise AttributeError("Configuration must be a non-empty dictionary") self._configuration = configuration diff --git a/featuremanagement/aio/_defaultfilters.py b/featuremanagement/aio/_defaultfilters.py index 2d6e5b8..556c926 100644 --- a/featuremanagement/aio/_defaultfilters.py +++ b/featuremanagement/aio/_defaultfilters.py @@ -20,7 +20,7 @@ class TimeWindowFilter(FeatureFilter): def __init__(self) -> None: self._filter = SyncTimeWindowFilter() - async def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: + async def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -40,7 +40,7 @@ class TargetingFilter(FeatureFilter): def __init__(self) -> None: self._filter = SyncTargetingFilter() - async def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: + async def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. diff --git a/featuremanagement/aio/_featurefilters.py b/featuremanagement/aio/_featurefilters.py index 986e3ea..5c2468d 100644 --- a/featuremanagement/aio/_featurefilters.py +++ b/featuremanagement/aio/_featurefilters.py @@ -13,7 +13,7 @@ class FeatureFilter(ABC): """ @abstractmethod - async def evaluate(self, context: Mapping[Any, Any], **kwargs: Dict[str, Any]) -> bool: + async def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. diff --git a/featuremanagement/aio/_featuremanager.py b/featuremanagement/aio/_featuremanager.py index 968c254..438e466 100644 --- a/featuremanagement/aio/_featuremanager.py +++ b/featuremanagement/aio/_featuremanager.py @@ -29,7 +29,7 @@ class FeatureManager(FeatureManagerBase): evaluated. """ - def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): + def __init__(self, configuration: Mapping[str, Any], **kwargs: Any): super().__init__(configuration, **kwargs) self._filters: Dict[str, FeatureFilter] = {} filters = [TimeWindowFilter(), TargetingFilter()] + cast( @@ -42,7 +42,7 @@ def __init__(self, configuration: Mapping[str, Any], **kwargs: Dict[str, Any]): self._filters[feature_filter.name] = feature_filter @overload # type: ignore - async def is_enabled(self, feature_flag_id: str, user_id: str, **kwargs: Dict[str, Any]) -> bool: + async def is_enabled(self, feature_flag_id: str, user_id: str, **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -52,7 +52,7 @@ async def is_enabled(self, feature_flag_id: str, user_id: str, **kwargs: Dict[st :rtype: bool """ - async def is_enabled(self, feature_flag_id: str, *args: Any, **kwargs: Dict[str, Any]) -> bool: # type: ignore + async def is_enabled(self, feature_flag_id: str, *args: Any, **kwargs: Any) -> bool: """ Determine if the feature flag is enabled for the given context. @@ -72,7 +72,7 @@ async def is_enabled(self, feature_flag_id: str, *args: Any, **kwargs: Dict[str, return result.enabled @overload # type: ignore - async def get_variant(self, feature_flag_id: str, user_id: str, **kwargs: Dict[str, Any]) -> Optional[Variant]: + async def get_variant(self, feature_flag_id: str, user_id: str, **kwargs: Any) -> Optional[Variant]: """ Determine the variant for the given context. @@ -82,9 +82,7 @@ async def get_variant(self, feature_flag_id: str, user_id: str, **kwargs: Dict[s :rtype: Variant """ - async def get_variant( # type: ignore - self, feature_flag_id: str, *args: Any, **kwargs: Dict[str, Any] - ) -> Optional[Variant]: + async def get_variant(self, feature_flag_id: str, *args: Any, **kwargs: Any) -> Optional[Variant]: """ Determine the variant for the given context. @@ -105,7 +103,7 @@ async def get_variant( # type: ignore return result.variant async def _check_feature_filters( - self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Dict[str, Any] + self, evaluation_event: EvaluationEvent, targeting_context: TargetingContext, **kwargs: Any ) -> None: feature_flag = evaluation_event.feature if not feature_flag: @@ -123,8 +121,8 @@ async def _check_feature_filters( for feature_filter in feature_filters: filter_name = feature_filter[FEATURE_FILTER_NAME] - kwargs["user"] = targeting_context.user_id # type: ignore - kwargs["groups"] = targeting_context.groups # type: ignore + kwargs["user"] = targeting_context.user_id + kwargs["groups"] = targeting_context.groups if filter_name not in self._filters: raise ValueError(f"Feature flag {feature_flag.name} has unknown filter {filter_name}") if feature_conditions.requirement_type == REQUIREMENT_TYPE_ALL: @@ -136,7 +134,7 @@ async def _check_feature_filters( break async def _check_feature( - self, feature_flag_id: str, targeting_context: TargetingContext, **kwargs: Dict[str, Any] + self, feature_flag_id: str, targeting_context: TargetingContext, **kwargs: Any ) -> EvaluationEvent: """ Determine if the feature flag is enabled for the given context. diff --git a/mypy.ini b/mypy.ini index 9cc9d19..3450f11 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,7 +7,7 @@ warn_unused_ignores = True # Getting these passing should be easy strict_equality = True -strict_concatenate = True +extra_checks = True # Strongly recommend enabling this one as soon as you can check_untyped_defs = True @@ -19,7 +19,7 @@ disallow_untyped_decorators = True disallow_any_generics = True # These next few are various gradations of forcing use of type annotations -disallow_untyped_calls = False +disallow_untyped_calls = True disallow_incomplete_defs = True disallow_untyped_defs = True From f749f54e26393ec8eeab4a0e16f6a59d7d04141f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 6 Aug 2024 10:34:00 -0700 Subject: [PATCH 3/5] Fixing alias type hint --- featuremanagement/_featurefilters.py | 11 +++++++---- featuremanagement/aio/_defaultfilters.py | 2 +- featuremanagement/aio/_featurefilters.py | 11 +++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/featuremanagement/_featurefilters.py b/featuremanagement/_featurefilters.py index 5f09ab1..97c7696 100644 --- a/featuremanagement/_featurefilters.py +++ b/featuremanagement/_featurefilters.py @@ -4,7 +4,8 @@ # license information. # ------------------------------------------------------------------------- from abc import ABC, abstractmethod -from typing import Mapping, Callable, Dict, Any +from typing import Mapping, Callable, Any, Optional +from typing_extensions import Self class FeatureFilter(ABC): @@ -12,6 +13,8 @@ class FeatureFilter(ABC): Parent class for all feature filters. """ + _alias: Optional[str] = None + @abstractmethod def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ @@ -28,8 +31,8 @@ def name(self) -> str: :return: Name of the filter, or alias if it exists. :rtype: str """ - if hasattr(self, "_alias"): - return self._alias # type: ignore + if hasattr(self, "_alias") and self._alias: + return self._alias return self.__class__.__name__ @staticmethod @@ -42,7 +45,7 @@ def alias(alias: str) -> Callable[..., Any]: :rtype: Callable """ - def wrapper(cls) -> Any: # type: ignore + def wrapper(cls: Self) -> Any: cls._alias = alias # pylint: disable=protected-access return cls diff --git a/featuremanagement/aio/_defaultfilters.py b/featuremanagement/aio/_defaultfilters.py index 556c926..ab8c1d8 100644 --- a/featuremanagement/aio/_defaultfilters.py +++ b/featuremanagement/aio/_defaultfilters.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import Mapping, Any, Dict +from typing import Mapping, Any from ._featurefilters import FeatureFilter from .._defaultfilters import ( TargetingFilter as SyncTargetingFilter, diff --git a/featuremanagement/aio/_featurefilters.py b/featuremanagement/aio/_featurefilters.py index 5c2468d..6718585 100644 --- a/featuremanagement/aio/_featurefilters.py +++ b/featuremanagement/aio/_featurefilters.py @@ -4,7 +4,8 @@ # license information. # ------------------------------------------------------------------------- from abc import ABC, abstractmethod -from typing import Mapping, Callable, Any, Dict +from typing import Mapping, Callable, Any, Optional +from typing_extensions import Self class FeatureFilter(ABC): @@ -12,6 +13,8 @@ class FeatureFilter(ABC): Parent class for all async feature filters. """ + _alias: Optional[str] = None + @abstractmethod async def evaluate(self, context: Mapping[Any, Any], **kwargs: Any) -> bool: """ @@ -28,8 +31,8 @@ def name(self) -> str: :return: Name of the filter, or alias if it exists. :rtype: str """ - if hasattr(self, "_alias"): - return self._alias # type: ignore + if hasattr(self, "_alias") and self._alias: + return self._alias return self.__class__.__name__ @staticmethod @@ -42,7 +45,7 @@ def alias(alias: str) -> Callable[..., Any]: :rtype: Callable """ - def wrapper(cls) -> Any: # type: ignore + def wrapper(cls: Self) -> Any: cls._alias = alias # pylint: disable=protected-access return cls From 83b3fc548555cd7ab707a6844f227008b65a8ed0 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 6 Aug 2024 10:41:12 -0700 Subject: [PATCH 4/5] wrong Mapping --- featuremanagement/_featuremanagerbase.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 3c65d93..1dc83ff 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -3,10 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from collections.abc import Mapping import hashlib from abc import ABC -from typing import List, Optional, Dict, Tuple, Any +from typing import List, Optional, Dict, Tuple, Any, Mapping from ._models import FeatureFlag, Variant, VariantAssignmentReason, TargetingContext, EvaluationEvent, VariantReference From 8f03caba23c5e99cc97f42dbe8b752c9ed6266a4 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 6 Aug 2024 10:45:51 -0700 Subject: [PATCH 5/5] Update validate.yml --- .github/workflows/validate.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 461c00c..2264a65 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -27,5 +27,8 @@ jobs: - name: Test with pytest run: | pytest tests --doctest-modules --cov-report=xml --cov-report=html + - name: Run mypy + run: | + mypy featuremanagement - name: cspell-action uses: streetsidesoftware/cspell-action@v6.8.0