From 09e6d10face80d98a4578ff54192b5c8bec007d7 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Fri, 9 Jul 2021 18:31:02 -0700 Subject: [PATCH] ACL uses enums, some more tests, other stuff. --- acl.py | 102 ++++++++++++++++++++++++++++------- argparse_utils.py | 3 ++ bootstrap.py | 2 + config.py | 3 ++ constants.py | 3 ++ dateparse/dateparse_utils.py | 21 +++----- datetime_utils.py | 18 ++++++- decorator_utils.py | 2 + deferred_operand.py | 3 ++ exceptions.py | 3 ++ id_generator.py | 3 ++ logging_utils.py | 2 + smart_future.py | 2 + tests/acl_test.py | 56 ++++++++++++++++--- thread_utils.py | 3 ++ 15 files changed, 185 insertions(+), 41 deletions(-) diff --git a/acl.py b/acl.py index 5040304..e6bb903 100644 --- a/acl.py +++ b/acl.py @@ -1,17 +1,25 @@ #!/usr/bin/env python3 from abc import ABC, abstractmethod +import enum import fnmatch import logging import re from typing import Any, Callable, List, Optional, Set +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. logger = logging.getLogger(__name__) -ACL_ORDER_ALLOW_DENY = 1 -ACL_ORDER_DENY_ALLOW = 2 +class Order(enum.Enum): + """A helper to express the order of evaluation for allows/denies + in an Access Control List. + """ + UNDEFINED = 0 + ALLOW_DENY = 1 + DENY_ALLOW = 2 class SimpleACL(ABC): @@ -20,22 +28,22 @@ class SimpleACL(ABC): def __init__( self, *, - order_to_check_allow_deny: int, + order_to_check_allow_deny: Order, default_answer: bool ): if order_to_check_allow_deny not in ( - ACL_ORDER_ALLOW_DENY, ACL_ORDER_DENY_ALLOW + Order.ALLOW_DENY, Order.DENY_ALLOW ): raise Exception( - 'order_to_check_allow_deny must be ACL_ORDER_ALLOW_DENY or ' + - 'ACL_ORDER_DENY_ALLOW') + 'order_to_check_allow_deny must be Order.ALLOW_DENY or ' + + 'Order.DENY_ALLOW') self.order_to_check_allow_deny = order_to_check_allow_deny self.default_answer = default_answer def __call__(self, x: Any) -> bool: """Returns True if x is allowed, False otherwise.""" logger.debug(f'SimpleACL checking {x}') - if self.order_to_check_allow_deny == ACL_ORDER_ALLOW_DENY: + if self.order_to_check_allow_deny == Order.ALLOW_DENY: logger.debug('Checking allowed first...') if self.check_allowed(x): logger.debug(f'{x} was allowed explicitly.') @@ -44,7 +52,7 @@ class SimpleACL(ABC): if self.check_denied(x): logger.debug(f'{x} was denied explicitly.') return False - elif self.order_to_check_allow_deny == ACL_ORDER_DENY_ALLOW: + elif self.order_to_check_allow_deny == Order.DENY_ALLOW: logger.debug('Checking denied first...') if self.check_denied(x): logger.debug(f'{x} was denied explicitly.') @@ -76,7 +84,7 @@ class SetBasedACL(SimpleACL): *, allow_set: Optional[Set[Any]] = None, deny_set: Optional[Set[Any]] = None, - order_to_check_allow_deny: int, + order_to_check_allow_deny: Order, default_answer: bool) -> None: super().__init__( order_to_check_allow_deny=order_to_check_allow_deny, @@ -96,13 +104,39 @@ class SetBasedACL(SimpleACL): return x in self.deny_set +class AllowListACL(SetBasedACL): + """Convenience subclass for a list that only allows known items. + i.e. a 'whitelist' + """ + def __init__(self, + *, + allow_set: Optional[Set[Any]]) -> None: + super().__init__( + allow_set = allow_set, + order_to_check_allow_deny = Order.ALLOW_DENY, + default_answer = False) + + +class DenyListACL(SetBasedACL): + """Convenience subclass for a list that only disallows known items. + i.e. a 'blacklist' + """ + def __init__(self, + *, + deny_set: Optional[Set[Any]]) -> None: + super().__init__( + deny_set = deny_set, + order_to_check_allow_deny = Order.ALLOW_DENY, + default_answer = True) + + class PredicateListBasedACL(SimpleACL): """An ACL that allows or denies by applying predicates.""" def __init__(self, *, allow_predicate_list: List[Callable[[Any], bool]] = None, deny_predicate_list: List[Callable[[Any], bool]] = None, - order_to_check_allow_deny: int, + order_to_check_allow_deny: Order, default_answer: bool) -> None: super().__init__( order_to_check_allow_deny=order_to_check_allow_deny, @@ -128,7 +162,7 @@ class StringWildcardBasedACL(PredicateListBasedACL): *, allowed_patterns: Optional[List[str]] = None, denied_patterns: Optional[List[str]] = None, - order_to_check_allow_deny: int, + order_to_check_allow_deny: Order, default_answer: bool) -> None: allow_predicates = [] if allowed_patterns is not None: @@ -158,7 +192,7 @@ class StringREBasedACL(PredicateListBasedACL): *, allowed_regexs: Optional[List[re.Pattern]] = None, denied_regexs: Optional[List[re.Pattern]] = None, - order_to_check_allow_deny: int, + order_to_check_allow_deny: Order, default_answer: bool) -> None: allow_predicates = None if allowed_regexs is not None: @@ -182,21 +216,49 @@ class StringREBasedACL(PredicateListBasedACL): ) -class AnyCompoundACL(object): +class AnyCompoundACL(SimpleACL): """An ACL that allows if any of its subacls allow.""" - def __init__(self, subacls: List[SimpleACL]): - assert subacls is not None + def __init__(self, + *, + subacls: Optional[List[SimpleACL]] = None, + order_to_check_allow_deny: Order, + default_answer: bool) -> None: + super().__init__( + order_to_check_allow_deny = order_to_check_allow_deny, + default_answer = default_answer + ) self.subacls = subacls - def __call__(self, x: Any): + def check_allowed(self, x: Any) -> bool: + if self.subacls is None: + return False return any(acl(x) for acl in self.subacls) + def check_denied(self, x: Any) -> bool: + if self.subacls is None: + return False + return any(not acl(x) for acl in self.subacls) + -class AllCompoundACL(object): +class AllCompoundACL(SimpleACL): """An ACL that allows if all of its subacls allow.""" - def __init__(self, subacls: List[SimpleACL]): - assert subacls is not None + def __init__(self, + *, + subacls: Optional[List[SimpleACL]] = None, + order_to_check_allow_deny: Order, + default_answer: bool) -> None: + super().__init__( + order_to_check_allow_deny = order_to_check_allow_deny, + default_answer = default_answer + ) self.subacls = subacls - def __call__(self, x: Any): + def check_allowed(self, x: Any) -> bool: + if self.subacls is None: + return False return all(acl(x) for acl in self.subacls) + + def check_denied(self, x: Any) -> bool: + if self.subacls is None: + return False + return any(not acl(x) for acl in self.subacls) diff --git a/argparse_utils.py b/argparse_utils.py index 02db0f0..3799a47 100644 --- a/argparse_utils.py +++ b/argparse_utils.py @@ -5,6 +5,9 @@ import datetime import logging import os +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. + logger = logging.getLogger(__name__) diff --git a/bootstrap.py b/bootstrap.py index 94c8e9c..da421b6 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -7,6 +7,8 @@ import sys import time import traceback +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. import argparse_utils import config diff --git a/config.py b/config.py index 58a5e83..e7094f3 100644 --- a/config.py +++ b/config.py @@ -71,6 +71,9 @@ import re import sys from typing import Any, Dict, List +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. + # Note: at this point in time, logging hasn't been configured and # anything we log will come out the root logger. diff --git a/constants.py b/constants.py index d321737..fdc533b 100644 --- a/constants.py +++ b/constants.py @@ -2,6 +2,9 @@ """Universal constants.""" +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. + # Date/time based constants SECONDS_PER_MINUTE = 60 SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE diff --git a/dateparse/dateparse_utils.py b/dateparse/dateparse_utils.py index ad92ccd..05bee8d 100755 --- a/dateparse/dateparse_utils.py +++ b/dateparse/dateparse_utils.py @@ -15,10 +15,13 @@ import pytz import acl import bootstrap -from decorator_utils import decorate_matching_methods_with +from datetime_utils import ( + TimeUnit, n_timeunits_from_base, datetime_to_date, date_to_datetime +) from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore +from decorator_utils import decorate_matching_methods_with logger = logging.getLogger(__name__) @@ -85,7 +88,7 @@ class RaisingErrorListener(antlr4.DiagnosticErrorListener): 'exit*', ], denied_patterns=None, - order_to_check_allow_deny=acl.ACL_ORDER_DENY_ALLOW, + order_to_check_allow_deny=acl.Order.DENY_ALLOW, default_answer=False ) ) @@ -105,7 +108,6 @@ class DateParser(dateparse_utilsListener): idea of "now" so that the code can be more easily unittested. Leave as None for real use cases. """ - from datetime_utils import TimeUnit self.month_name_to_number = { 'jan': 1, 'feb': 2, @@ -231,7 +233,6 @@ class DateParser(dateparse_utilsListener): def _reset(self): """Reset at init and between parses.""" - from datetime_utils import datetime_to_date if self.override_now_for_test_purposes is None: self.now_datetime = datetime.datetime.now() self.today = datetime.date.today() @@ -259,9 +260,8 @@ class DateParser(dateparse_utilsListener): name = name.replace('washi', 'presi') return name - def _figure_out_date_unit(self, orig: str) -> int: + def _figure_out_date_unit(self, orig: str) -> TimeUnit: """Figure out what unit a date expression piece is talking about.""" - from datetime_utils import TimeUnit if 'month' in orig: return TimeUnit.MONTHS txt = orig.lower()[:3] @@ -469,9 +469,6 @@ class DateParser(dateparse_utilsListener): def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None: """When we leave the date expression, populate self.date.""" - from datetime_utils import ( - n_timeunits_from_base, datetime_to_date, date_to_datetime - ) if 'special' in self.context: self.date = self._parse_special_date(self.context['special']) else: @@ -514,7 +511,6 @@ class DateParser(dateparse_utilsListener): def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None: # Simple time? - from datetime_utils import TimeUnit self.time = datetime.time( self.context['hour'], self.context['minute'], @@ -641,7 +637,6 @@ class DateParser(dateparse_utilsListener): def exitDeltaTimeFraction( self, ctx: dateparse_utilsParser.DeltaTimeFractionContext ) -> None: - from datetime_utils import TimeUnit try: txt = ctx.getText().lower()[:4] if txt == 'quar': @@ -874,7 +869,6 @@ class DateParser(dateparse_utilsListener): def exitNFoosFromTodayAgoExpr( self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext ) -> None: - from datetime_utils import n_timeunits_from_base d = self.now_datetime try: count = self._get_int(ctx.unsignedInt().getText()) @@ -900,7 +894,6 @@ class DateParser(dateparse_utilsListener): def exitDeltaRelativeToTodayExpr( self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext ) -> None: - from datetime_utils import n_timeunits_from_base d = self.now_datetime try: mod = ctx.thisNextLast() @@ -1055,6 +1048,7 @@ class DateParser(dateparse_utilsListener): pass +@bootstrap.initialize def main() -> None: parser = DateParser() for line in sys.stdin: @@ -1072,5 +1066,4 @@ def main() -> None: if __name__ == "__main__": - main = bootstrap.initialize(main) main() diff --git a/datetime_utils.py b/datetime_utils.py index 0b94283..795b427 100644 --- a/datetime_utils.py +++ b/datetime_utils.py @@ -6,7 +6,7 @@ import datetime import enum import logging import re -from typing import NewType, Tuple +from typing import Any, NewType, Tuple import holidays # type: ignore import pytz @@ -77,12 +77,28 @@ class TimeUnit(enum.Enum): MONTHS = 13 YEARS = 14 + @classmethod + def is_valid(cls, value: Any): + if type(value) is int: + print("int") + return value in cls._value2member_map_ + elif type(value) is TimeUnit: + print("TimeUnit") + return value.value in cls._value2member_map_ + elif type(value) is str: + print("str") + return value in cls._member_names_ + else: + print(type(value)) + return False + def n_timeunits_from_base( count: int, unit: TimeUnit, base: datetime.datetime ) -> datetime.datetime: + assert TimeUnit.is_valid(unit) if count == 0: return base diff --git a/decorator_utils.py b/decorator_utils.py index 375cbad..2817239 100644 --- a/decorator_utils.py +++ b/decorator_utils.py @@ -18,6 +18,8 @@ import traceback from typing import Callable, Optional import warnings +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. import exceptions diff --git a/deferred_operand.py b/deferred_operand.py index f2af66c..4b12279 100644 --- a/deferred_operand.py +++ b/deferred_operand.py @@ -3,6 +3,9 @@ from abc import ABC, abstractmethod from typing import Any, Generic, TypeVar +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. + T = TypeVar('T') diff --git a/exceptions.py b/exceptions.py index 3e0a2d0..59aa262 100644 --- a/exceptions.py +++ b/exceptions.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. + class PreconditionException(AssertionError): pass diff --git a/id_generator.py b/id_generator.py index cc287bb..c5a0d93 100644 --- a/id_generator.py +++ b/id_generator.py @@ -3,6 +3,9 @@ import itertools import logging +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. + logger = logging.getLogger(__name__) generators = {} diff --git a/logging_utils.py b/logging_utils.py index ba5270f..700bfab 100644 --- a/logging_utils.py +++ b/logging_utils.py @@ -10,6 +10,8 @@ import os import pytz import sys +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. import argparse_utils import config diff --git a/smart_future.py b/smart_future.py index e4832d4..1c95973 100644 --- a/smart_future.py +++ b/smart_future.py @@ -6,6 +6,8 @@ import concurrent.futures as fut import time from typing import Callable, List, TypeVar +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. from deferred_operand import DeferredOperand import id_generator diff --git a/tests/acl_test.py b/tests/acl_test.py index 810aedf..4c1cf21 100755 --- a/tests/acl_test.py +++ b/tests/acl_test.py @@ -13,7 +13,7 @@ class TestSimpleACL(unittest.TestCase): even = acl.SetBasedACL( allow_set = set([2, 4, 6, 8, 10]), deny_set = set([1, 3, 5, 7, 9]), - order_to_check_allow_deny = acl.ACL_ORDER_ALLOW_DENY, + order_to_check_allow_deny = acl.Order.ALLOW_DENY, default_answer = False ) self.assertTrue(even(2)) @@ -23,12 +23,12 @@ class TestSimpleACL(unittest.TestCase): def test_wildcard_based_acl(self): a_or_b = acl.StringWildcardBasedACL( allowed_patterns = ['a*', 'b*'], - order_to_check_allow_deny = acl.ACL_ORDER_ALLOW_DENY, + order_to_check_allow_deny = acl.Order.ALLOW_DENY, default_answer = False ) self.assertTrue(a_or_b('aardvark')) - self.assertTrue(a_or_b('bubblegum')) - self.assertFalse(a_or_b('charlie')) + self.assertTrue(a_or_b('baboon')) + self.assertFalse(a_or_b('cheetah')) def test_re_based_acl(self): weird = acl.StringREBasedACL( @@ -36,12 +36,56 @@ class TestSimpleACL(unittest.TestCase): re.compile('^a.*a$'), re.compile('^b.*b$') ], - order_to_check_allow_deny = acl.ACL_ORDER_ALLOW_DENY, + order_to_check_allow_deny = acl.Order.DENY_ALLOW, default_answer = True ) self.assertTrue(weird('aardvark')) self.assertFalse(weird('anaconda')) - self.assertFalse(weird('beelzebub')) + self.assertFalse(weird('blackneb')) + self.assertTrue(weird('crow')) + + def test_compound_acls_disjunction(self): + a_b_c = acl.StringWildcardBasedACL( + allowed_patterns = ['a*', 'b*', 'c*'], + order_to_check_allow_deny = acl.Order.ALLOW_DENY, + default_answer = False + ) + c_d_e = acl.StringWildcardBasedACL( + allowed_patterns = ['c*', 'd*', 'e*'], + order_to_check_allow_deny = acl.Order.ALLOW_DENY, + default_answer = False + ) + disjunction = acl.AnyCompoundACL( + subacls = [a_b_c, c_d_e], + order_to_check_allow_deny = acl.Order.ALLOW_DENY, + default_answer = False, + ) + self.assertTrue(disjunction('aardvark')) + self.assertTrue(disjunction('caribou')) + self.assertTrue(disjunction('eagle')) + self.assertFalse(disjunction('newt')) + + def test_compound_acls_conjunction(self): + a_b_c = acl.StringWildcardBasedACL( + allowed_patterns = ['a*', 'b*', 'c*'], + order_to_check_allow_deny = acl.Order.ALLOW_DENY, + default_answer = False + ) + c_d_e = acl.StringWildcardBasedACL( + allowed_patterns = ['c*', 'd*', 'e*'], + order_to_check_allow_deny = acl.Order.ALLOW_DENY, + default_answer = False + ) + conjunction = acl.AllCompoundACL( + subacls = [a_b_c, c_d_e], + order_to_check_allow_deny = acl.Order.ALLOW_DENY, + default_answer = False, + ) + self.assertFalse(conjunction('aardvark')) + self.assertTrue(conjunction('caribou')) + self.assertTrue(conjunction('condor')) + self.assertFalse(conjunction('eagle')) + self.assertFalse(conjunction('newt')) if __name__ == '__main__': diff --git a/thread_utils.py b/thread_utils.py index af6e0e1..bb15c03 100644 --- a/thread_utils.py +++ b/thread_utils.py @@ -6,6 +6,9 @@ import os import threading from typing import Callable, Optional, Tuple +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. + logger = logging.getLogger(__name__) -- 2.46.0