diff --git a/.circleci/config.yml b/.circleci/config.yml index 910d23c..a263e53 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,11 @@ jobs: steps: - checkout + - run: + name: Install PostgreSQL + command: | + sudo apt-get update && sudo apt-get install -y postgresql postgresql-client + - restore_cache: keys: - v1-dependencies-{{ checksum "setup.py" }} diff --git a/.isort.cfg b/.isort.cfg index edc9755..4bc495b 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -4,4 +4,4 @@ include_trailing_comma=True force_grid_wrap=0 combine_as_imports=True line_length=88 -known_third_party = boto3,cachetools,fakeredis,moto,pyee,pytest,redis,setuptools,testing,thrift +known_third_party = boto3,cachetools,fakeredis,moto,psycopg,pyee,pytest,pytest_postgresql,redis,setuptools,thrift diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cccb658..975d2c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: hooks: - id: insert-license files: flipper|flipper_thrift - - repo: https://gitlab.com/pycqa/flake8 + - repo: https://github.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 diff --git a/README.md b/README.md index b5f599e..862055c 100644 --- a/README.md +++ b/README.md @@ -895,7 +895,7 @@ This project is actively maintained by the maintainers listed in the MAINTAINERS ## Contacts -The project maintainers can be reached via email at adam.savitzky@carta.com or luis.montiel@carta.com. +The project maintainers can be reached via email at luis.montiel@carta.com. ## Discussion diff --git a/flipper/contrib/cached.py b/flipper/contrib/cached.py index 0f8dd4c..ac0c3d6 100644 --- a/flipper/contrib/cached.py +++ b/flipper/contrib/cached.py @@ -13,7 +13,7 @@ from typing import Iterator, Optional -from cachetools import LRUCache, TTLCache +from cachetools import LRUCache, TTLCache # type: ignore from .interface import AbstractFeatureFlagStore from .storage import FeatureFlagStoreItem, FeatureFlagStoreMeta diff --git a/flipper/contrib/redis.py b/flipper/contrib/redis.py index 84942dd..f314f7c 100644 --- a/flipper/contrib/redis.py +++ b/flipper/contrib/redis.py @@ -13,7 +13,7 @@ from typing import Iterator, Optional -from redis import Redis +from redis import Redis # type: ignore from .interface import AbstractFeatureFlagStore, FlagDoesNotExistError from .storage import FeatureFlagStoreItem, FeatureFlagStoreMeta diff --git a/pyproject.toml b/pyproject.toml index d183784..6cdc68c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,3 +16,7 @@ exclude = ''' | dist )/ ''' + +[tool.mypy] +namespace_packages = true +explicit_package_bases = true diff --git a/setup.py b/setup.py index 1aa841f..222824a 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name="flipper-client", - version="1.3.2", + version="1.3.3", packages=find_packages(), license="Apache License 2.0", long_description=open("README.md").read(), @@ -24,20 +24,21 @@ "six>=1.12", "fakeredis~=1.0", "pytest~=7.1.0", - "ipython", - "thrift", + "ipython~=8.0", + "thrift~=0.22", "setuptools", "wheel", "ipdb", - "black==22.1.0", + "black~=22.3", "pre-commit", - "isort", - "flake8", - "mypy", - "moto", - "bandit", - "twine", - "testing.postgresql", + "isort~=6.0", + "flake8~=7.2", + "mypy~=1.0", + "moto~=4.2", + "bandit~=1.8", + "twine~=6.1", + "pytest-postgresql~=6.0", + "pytest~=7.1", ], }, classifiers=["License :: OSI Approved :: Apache Software License"], diff --git a/tests/bucketing/test_consistent_hash_percentage_bucketer.py b/tests/bucketing/test_consistent_hash_percentage_bucketer.py index d604333..09c857f 100644 --- a/tests/bucketing/test_consistent_hash_percentage_bucketer.py +++ b/tests/bucketing/test_consistent_hash_percentage_bucketer.py @@ -39,28 +39,28 @@ def test_always_returns_false_when_percentage_is_zero(self): self.assertFalse(bucketer.check()) # score = 0.32 def test_returns_true_when_conditions_hash_to_value_less_than_percentage( - self + self, ): # noqa: E501 percentage = Percentage(value=0.8) bucketer = ConsistentHashPercentageBucketer(percentage=percentage) self.assertTrue(bucketer.check(foo="bar")) # score = 0.79 def test_returns_false_when_conditions_hash_to_value_greater_than_percentage( - self + self, ): # noqa: E501 percentage = Percentage(value=0.5) bucketer = ConsistentHashPercentageBucketer(percentage=percentage) self.assertFalse(bucketer.check(foo="bar")) # score = 0.79 def test_returns_true_when_conditions_hash_to_value_equal_to_percentage( - self + self, ): # noqa: E501 percentage = Percentage(value=0.79) bucketer = ConsistentHashPercentageBucketer(percentage=percentage) self.assertTrue(bucketer.check(foo="bar")) # score = 0.79 def test_always_returns_false_when_percentage_is_zero_when_including_conditions( - self + self, ): # noqa: E501 percentage = Percentage(value=0.0) bucketer = ConsistentHashPercentageBucketer(percentage=percentage) diff --git a/tests/conditions/operators/test_greater_than_or_equal_to_operator.py b/tests/conditions/operators/test_greater_than_or_equal_to_operator.py index 14d44cb..fe3aff3 100644 --- a/tests/conditions/operators/test_greater_than_or_equal_to_operator.py +++ b/tests/conditions/operators/test_greater_than_or_equal_to_operator.py @@ -5,7 +5,7 @@ import unittest from flipper.conditions.operators.greater_than_or_equal_to_operator import ( - GreaterThanOrEqualToOperator + GreaterThanOrEqualToOperator, ) diff --git a/tests/conditions/operators/test_less_than_or_equal_to_operator.py b/tests/conditions/operators/test_less_than_or_equal_to_operator.py index 6dc0f45..d00e8af 100644 --- a/tests/conditions/operators/test_less_than_or_equal_to_operator.py +++ b/tests/conditions/operators/test_less_than_or_equal_to_operator.py @@ -5,7 +5,7 @@ import unittest from flipper.conditions.operators.less_than_or_equal_to_operator import ( - LessThanOrEqualToOperator + LessThanOrEqualToOperator, ) diff --git a/tests/conditions/operators/test_negated_set_membership_operator.py b/tests/conditions/operators/test_negated_set_membership_operator.py index 88d2166..20a73f3 100644 --- a/tests/conditions/operators/test_negated_set_membership_operator.py +++ b/tests/conditions/operators/test_negated_set_membership_operator.py @@ -5,7 +5,7 @@ import unittest from flipper.conditions.operators.negated_set_membership_operator import ( - NegatedSetMembershipOperator + NegatedSetMembershipOperator, ) diff --git a/tests/conditions/test_check.py b/tests/conditions/test_check.py index 3726686..e038adf 100644 --- a/tests/conditions/test_check.py +++ b/tests/conditions/test_check.py @@ -10,14 +10,14 @@ from flipper.conditions.operators.equality_operator import EqualityOperator from flipper.conditions.operators.greater_than_operator import GreaterThanOperator from flipper.conditions.operators.greater_than_or_equal_to_operator import ( - GreaterThanOrEqualToOperator + GreaterThanOrEqualToOperator, ) from flipper.conditions.operators.less_than_operator import LessThanOperator from flipper.conditions.operators.less_than_or_equal_to_operator import ( - LessThanOrEqualToOperator + LessThanOrEqualToOperator, ) from flipper.conditions.operators.negated_set_membership_operator import ( - NegatedSetMembershipOperator + NegatedSetMembershipOperator, ) from flipper.conditions.operators.negation_operator import NegationOperator from flipper.conditions.operators.set_membership_operator import SetMembershipOperator diff --git a/tests/contrib/storage/test_item.py b/tests/contrib/storage/test_item.py index fe393b9..b7bcf98 100644 --- a/tests/contrib/storage/test_item.py +++ b/tests/contrib/storage/test_item.py @@ -147,7 +147,7 @@ def test_returns_true_if_bucketer_check_returns_true(self): self.assertTrue(item.is_enabled()) def test_returns_false_when_bucketer_returns_false_and_conditions_not_specified( - self + self, ): # noqa: E501 # flag.is_enabled(user_id=2) # False bucketer = MagicMock() @@ -159,7 +159,7 @@ def test_returns_false_when_bucketer_returns_false_and_conditions_not_specified( self.assertFalse(item.is_enabled()) def test_returns_true_when_bucketer_returns_false_and_conditions_return_true( - self + self, ): # noqa: E501 # flag.is_enabled(user_id=2, is_admin=True) # True bucketer = MagicMock() @@ -171,7 +171,7 @@ def test_returns_true_when_bucketer_returns_false_and_conditions_return_true( self.assertTrue(item.is_enabled(is_admin=True)) def test_returns_true_when_bucketer_returns_true_and_conditions_not_specified( - self + self, ): # noqa: E501 # flag.is_enabled(user_id=1) # True bucketer = MagicMock() @@ -183,7 +183,7 @@ def test_returns_true_when_bucketer_returns_true_and_conditions_not_specified( self.assertTrue(item.is_enabled()) def test_returns_false_when_bucketer_returns_true_and_conditions_return_false( - self + self, ): # noqa: E501 # flag.is_enabled(user_id=1, is_admin=False) # False bucketer = MagicMock() diff --git a/tests/contrib/test_postgresql.py b/tests/contrib/test_postgresql.py index 6b62cda..6d062b7 100644 --- a/tests/contrib/test_postgresql.py +++ b/tests/contrib/test_postgresql.py @@ -1,147 +1,140 @@ -import unittest from typing import Iterable -import testing.postgresql +import psycopg +import pytest +from pytest_postgresql.factories import postgresql_proc from flipper import PostgreSQLFeatureFlagStore from flipper.contrib.interface import FlagDoesNotExistError from flipper.contrib.storage import FeatureFlagStoreMeta from flipper.contrib.util.date import now -Postgresql = testing.postgresql.PostgresqlFactory(cache_initialized_db=True) +postgresql = postgresql_proc(port=None) -def tearDownModule(self): - Postgresql.clear_cache() +@pytest.fixture +def postgresql_db(postgresql): + return postgresql -class BaseTest(unittest.TestCase): - def setUp(self): - self._db = Postgresql() - self.store = PostgreSQLFeatureFlagStore(self._db.url()) +@pytest.fixture(autouse=True) +def clean_db(postgresql): + conninfo = ( + f"postgresql://{postgresql.user}:{postgresql.password}@{postgresql.host}" + f":{postgresql.port}/postgres?gssencmode=disable" + ) + with psycopg.connect(conninfo) as conn: + with conn.cursor() as cur: + cur.execute("DROP TABLE IF EXISTS feature_flags;") + conn.commit() + yield - def tearDown(self): - self._db.stop() +@pytest.fixture +def store(postgresql): + conninfo = ( + f"postgresql://{postgresql.user}:{postgresql.password}@{postgresql.host}" + f":{postgresql.port}/postgres?gssencmode=disable" + ) + store = PostgreSQLFeatureFlagStore(conninfo) + return store -class TestRunMigration(unittest.TestCase): - def test_run_migration_creates_table(self): - db = Postgresql() - store = PostgreSQLFeatureFlagStore(db.url(), run_migrations=False) +class TestRunMigration: + def test_run_migration_creates_table(self, postgresql): + conninfo = ( + f"postgresql://{postgresql.user}:{postgresql.password}" + f"@{postgresql.host}:{postgresql.port}/postgres?gssencmode=disable" + ) + store = PostgreSQLFeatureFlagStore(conninfo, run_migrations=False) store.run_migrations() + assert store.get("") is None - self.assertIsNone(store.get("")) - -class TestCreate(BaseTest): - def test_feature_flag_exists_when_created(self): +class TestCreate: + def test_feature_flag_exists_when_created(self, store): feature_name = "test" + item = store.create(feature_name) + assert item is not None - item = self.store.create(feature_name) - - self.assertIsNotNone(item) - - def test_create_overrides_when_existing_feature_flag(self): + def test_create_overrides_when_existing_feature_flag(self, store): feature_name = "test" + store.create(feature_name) + new_item = store.create(feature_name, client_data={"test": "data"}) + assert new_item.meta == store.get(feature_name).meta - self.store.create(feature_name) - new_item = self.store.create(feature_name, client_data={"test": "data"}) - - self.assertEqual(new_item.meta, self.store.get(feature_name).meta) - - def test_is_enabled_is_false_when_created_with_default(self): - item = self.store.create("test") - - self.assertFalse(item.is_enabled()) - - def test_is_enabled_is_true_when_created_with_is_enabled(self): - item = self.store.create("test", is_enabled=True) + def test_is_enabled_is_false_when_created_with_default(self, store): + item = store.create("test") + assert not item.is_enabled() - self.assertTrue(item.is_enabled()) + def test_is_enabled_is_true_when_created_with_is_enabled(self, store): + item = store.create("test", is_enabled=True) + assert item.is_enabled() - def test_is_enabled_is_false_when_created_with_not_is_enabled(self): - item = self.store.create("test", is_enabled=False) + def test_is_enabled_is_false_when_created_with_not_is_enabled(self, store): + item = store.create("test", is_enabled=False) + assert not item.is_enabled() - self.assertFalse(item.is_enabled()) - - def test_client_data_is_persisted_when_created_with_client_data(self): + def test_client_data_is_persisted_when_created_with_client_data(self, store): client_data = {"test": "data"} + item = store.create("test", client_data=client_data) + assert item.meta["client_data"] == client_data - item = self.store.create("test", client_data=client_data) - - self.assertEqual(item.meta["client_data"], client_data) - - -class TestGet(BaseTest): - def test_returns_none_when_no_such_feature_flag(self): - item = self.store.get("test") - self.assertIsNone(item) +class TestGet: + def test_returns_none_when_no_such_feature_flag(self, store): + item = store.get("test") + assert item is None -class TestList(BaseTest): - def _create_several(self, names: Iterable[str]): +class TestList: + def _create_several(self, store, names: Iterable[str]): for name in names: - self.store.create(name) + store.create(name) - def test_returns_empty_iterator_when_no_feature_flags(self): - items = list(self.store.list()) + def test_returns_empty_iterator_when_no_feature_flags(self, store): + items = list(store.list()) + assert len(items) == 0 - self.assertEqual(len(items), 0) - - def test_returns_feature_flags(self): + def test_returns_feature_flags(self, store): expected_names = {"test1", "test2"} - self._create_several(expected_names) - - names = {x.feature_name for x in self.store.list()} - - self.assertSetEqual(names, expected_names) + self._create_several(store, expected_names) + names = {x.feature_name for x in store.list()} + assert names == expected_names - def test_limits_return_items_when_limit_is_given(self): - self._create_several({"test1", "test2"}) + def test_limits_return_items_when_limit_is_given(self, store): + self._create_several(store, {"test1", "test2"}) + items = list(store.list(limit=1)) + assert len(items) == 1 - items = list(self.store.list(limit=1)) + def test_starts_with_offset_when_offset_is_given(self, store): + self._create_several(store, {"test1", "test2"}) + items = list(store.list(offset=1)) + assert len(items) == 1 - self.assertEqual(len(items), 1) - def test_starts_with_offset_when_offset_is_given(self): - self._create_several({"test1", "test2"}) - - items = list(self.store.list(offset=1)) - - self.assertEqual(len(items), 1) - - -class TestSetMeta(BaseTest): - def test_raises_exception_for_nonexistent_flag(self): +class TestSetMeta: + def test_raises_exception_for_nonexistent_flag(self, store): meta = FeatureFlagStoreMeta(now()) + with pytest.raises(FlagDoesNotExistError): + store.set_meta("test", meta) - with self.assertRaises(FlagDoesNotExistError): - self.store.set_meta("test", meta) - - def test_updated_meta(self): - self.store.create("test") + def test_updated_meta(self, store): + store.create("test") expected_meta = FeatureFlagStoreMeta(now(), client_data={"test": "date"}) - - self.store.set_meta("test", expected_meta) - - meta = self.store.get("test").meta - self.assertEqual(meta, expected_meta.to_dict()) - - -class TestDelete(BaseTest): - def test_does_not_raise_exception_when_no_existing_flag(self): - item = self.store.get("test") - - self.assertIsNone(item) - - self.store.delete("test") - - def test_deletes_existing_flag(self): - self.store.create("test") - - self.store.delete("test") - - item = self.store.get("test") - self.assertIsNone(item) + store.set_meta("test", expected_meta) + meta = store.get("test").meta + assert meta == expected_meta.to_dict() + + +class TestDelete: + def test_does_not_raise_exception_when_no_existing_flag(self, store): + item = store.get("test") + assert item is None + store.delete("test") + + def test_deletes_existing_flag(self, store): + store.create("test") + store.delete("test") + item = store.get("test") + assert item is None diff --git a/tests/contrib/test_redis.py b/tests/contrib/test_redis.py index 5b7b3db..787c0b0 100644 --- a/tests/contrib/test_redis.py +++ b/tests/contrib/test_redis.py @@ -225,7 +225,7 @@ def test_returns_features_subject_to_offset_and_limit(self): self.assertTrue(feature_name in feature_names) def test_when_batch_size_is_set_to_a_value_smaller_than_number_of_keys_still_returns_everything( # noqa: E501 - self + self, ): feature_names = [self.txt() for _ in range(10)]