def test_all_enclosing_scopes(self): """`all_enclosing_scopes` should repeatedly apply `enclosing_scope` to any valid single- or multiple- component scope. `all_enclosing_scopes` should not yield the global scope if `allow_global=False`.""" global_closure = list( all_enclosing_scopes(GLOBAL_SCOPE, allow_global=True)) self.assertEqual(global_closure, [GLOBAL_SCOPE]) global_closure_excluded = list( all_enclosing_scopes(GLOBAL_SCOPE, allow_global=False)) self.assertEqual(global_closure_excluded, []) base_scope = 'scope' base_scope_closure = list(all_enclosing_scopes(base_scope)) self.assertEqual(base_scope_closure, [base_scope, GLOBAL_SCOPE]) subscope = 'subscope' compound_scope = '{}.{}'.format(base_scope, subscope) compound_scope_closure = list(all_enclosing_scopes(compound_scope)) self.assertEqual(compound_scope_closure, [compound_scope, base_scope, GLOBAL_SCOPE]) compound_scope_closure_no_global = list( all_enclosing_scopes(compound_scope, allow_global=False)) self.assertEqual(compound_scope_closure_no_global, [compound_scope, base_scope])
def test_valid_invalid_scope(self) -> None: """Scopes with dashes or underscores are treated as a single component, and scopes with empty components raise an InvalidScopeError.""" base_dashed_scope = "base-scope" self.assertEqual(enclosing_scope(base_dashed_scope), GLOBAL_SCOPE) subscope_underscore = "sub_scope" self.assertEqual(enclosing_scope(subscope_underscore), GLOBAL_SCOPE) compound_scope = f"{base_dashed_scope}.{subscope_underscore}" self.assertEqual(enclosing_scope(compound_scope), base_dashed_scope) self.assertEqual( list(all_enclosing_scopes(compound_scope)), [ compound_scope, base_dashed_scope, GLOBAL_SCOPE, ], ) invalid_scope = "a.b..c.d" with self.assertRaises(InvalidScopeError): enclosing_scope(invalid_scope) with self.assertRaises(InvalidScopeError): # need to bounce to list to get it to error since this is a generator list(all_enclosing_scopes(invalid_scope))
def complete_scopes(cls, scope_infos): """Expand a set of scopes to include all enclosing scopes. E.g., if the set contains `foo.bar.baz`, ensure that it also contains `foo.bar` and `foo`. Also adds any deprecated scopes. """ ret = {GlobalOptionsRegistrar.get_scope_info()} original_scopes = dict() for si in scope_infos: ret.add(si) if si.scope in original_scopes: raise cls.DuplicateScopeError('Scope `{}` claimed by {}, was also claimed by {}.'.format( si.scope, si, original_scopes[si.scope] )) original_scopes[si.scope] = si if si.deprecated_scope: ret.add(ScopeInfo(si.deprecated_scope, si.category, si.optionable_cls)) original_scopes[si.deprecated_scope] = si # TODO: Once scope name validation is enforced (so there can be no dots in scope name # components) we can replace this line with `for si in scope_infos:`, because it will # not be possible for a deprecated_scope to introduce any new intermediate scopes. for si in copy.copy(ret): for scope in all_enclosing_scopes(si.scope, allow_global=False): if scope not in original_scopes: ret.add(ScopeInfo(scope, ScopeInfo.INTERMEDIATE)) return ret
def complete_scopes( cls, scope_infos: Iterable[ScopeInfo]) -> FrozenOrderedSet[ScopeInfo]: """Expand a set of scopes to include all enclosing scopes. E.g., if the set contains `foo.bar.baz`, ensure that it also contains `foo.bar` and `foo`. Also adds any deprecated scopes. """ ret: OrderedSet[ScopeInfo] = OrderedSet() original_scopes: Dict[str, ScopeInfo] = {} for si in sorted(scope_infos, key=lambda si: si.scope): ret.add(si) if si.scope in original_scopes: raise cls.DuplicateScopeError( "Scope `{}` claimed by {}, was also claimed by {}.".format( si.scope, si, original_scopes[si.scope])) original_scopes[si.scope] = si if si.deprecated_scope: ret.add( ScopeInfo(si.deprecated_scope, si.category, si.optionable_cls)) original_scopes[si.deprecated_scope] = si # TODO: Once scope name validation is enforced (so there can be no dots in scope name # components) we can replace this line with `for si in scope_infos:`, because it will # not be possible for a deprecated_scope to introduce any new intermediate scopes. for si in copy.copy(ret): for scope in all_enclosing_scopes(si.scope, allow_global=False): if scope not in original_scopes: ret.add(ScopeInfo(scope, ScopeInfo.INTERMEDIATE)) return FrozenOrderedSet(ret)
def get_fingerprintable_for_scope( self, bottom_scope: str, include_passthru: Optional[bool] = None, fingerprint_key: str = "fingerprint", invert: bool = False, ): """Returns a list of fingerprintable (option type, option value) pairs for the given scope. Fingerprintable options are options registered via a "fingerprint=True" kwarg. This flag can be parameterized with `fingerprint_key` for special cases. This method also searches enclosing options scopes of `bottom_scope` to determine the set of fingerprintable pairs. :param bottom_scope: The scope to gather fingerprintable options for. :param include_passthru: Whether to include passthru args captured by `bottom_scope` in the fingerprintable options. :param fingerprint_key: The option kwarg to match against (defaults to 'fingerprint'). :param invert: Whether or not to invert the boolean check for the fingerprint_key value. :API: public """ deprecated_conditional( predicate=lambda: include_passthru is not None, removal_version="1.31.0.dev0", entity_description="get_fingerprintable_for_scope `include_passthru` arg", hint_message=( "passthru arguments are fingerprinted if their associated option value is." ), ) fingerprint_default = bool(invert) pairs = [] # Note that we iterate over options registered at `bottom_scope` and at all # enclosing scopes, since option-using code can read those values indirectly # via its own OptionValueContainer, so they can affect that code's output. for registration_scope in all_enclosing_scopes(bottom_scope): parser = self._parser_hierarchy.get_parser_by_scope(registration_scope) # Sort the arguments, so that the fingerprint is consistent. for (_, kwargs) in sorted(parser.option_registrations_iter()): if kwargs.get("recursive", False) and not kwargs.get("recursive_root", False): continue # We only need to fprint recursive options once. if not kwargs.get(fingerprint_key, fingerprint_default): continue # Note that we read the value from scope, even if the registration was on an enclosing # scope, to get the right value for recursive options (and because this mirrors what # option-using code does). val = self.for_scope(bottom_scope)[kwargs["dest"]] # If we have a list then we delegate to the fingerprinting implementation of the members. if is_list_option(kwargs): val_type = kwargs.get("member_type", str) else: val_type = kwargs.get("type", str) pairs.append((val_type, val)) return pairs
def get_fingerprintable_for_scope(self, bottom_scope, include_passthru=False, fingerprint_key=None, invert=False): """Returns a list of fingerprintable (option type, option value) pairs for the given scope. Fingerprintable options are options registered via a "fingerprint=True" kwarg. This flag can be parameterized with `fingerprint_key` for special cases. This method also searches enclosing options scopes of `bottom_scope` to determine the set of fingerprintable pairs. :param str bottom_scope: The scope to gather fingerprintable options for. :param bool include_passthru: Whether to include passthru args captured by `bottom_scope` in the fingerprintable options. :param string fingerprint_key: The option kwarg to match against (defaults to 'fingerprint'). :param bool invert: Whether or not to invert the boolean check for the fingerprint_key value. :API: public """ fingerprint_key = fingerprint_key or 'fingerprint' fingerprint_default = bool(invert) pairs = [] if include_passthru: # Passthru args can only be sent to outermost scopes so we gather them once here up-front. passthru_args = self.passthru_args_for_scope(bottom_scope) # NB: We can't sort passthru args, the underlying consumer may be order-sensitive. pairs.extend((str, pass_arg) for pass_arg in passthru_args) # Note that we iterate over options registered at `bottom_scope` and at all # enclosing scopes, since option-using code can read those values indirectly # via its own OptionValueContainer, so they can affect that code's output. for registration_scope in all_enclosing_scopes(bottom_scope): parser = self._parser_hierarchy.get_parser_by_scope( registration_scope) # Sort the arguments, so that the fingerprint is consistent. for (_, kwargs) in sorted(parser.option_registrations_iter()): if kwargs.get('recursive', False) and not kwargs.get( 'recursive_root', False): continue # We only need to fprint recursive options once. if kwargs.get(fingerprint_key, fingerprint_default) is not True: continue # Note that we read the value from scope, even if the registration was on an enclosing # scope, to get the right value for recursive options (and because this mirrors what # option-using code does). val = self.for_scope(bottom_scope)[kwargs['dest']] # If we have a list then we delegate to the fingerprinting implementation of the members. if is_list_option(kwargs): val_type = kwargs.get('member_type', str) else: val_type = kwargs.get('type', str) pairs.append((val_type, val)) return pairs
def test_all_enclosing_scopes(self): """`all_enclosing_scopes` should repeatedly apply `enclosing_scope` to any valid single- or multiple- component scope. `all_enclosing_scopes` should not yield the global scope if `allow_global=False`.""" global_closure = list(all_enclosing_scopes(GLOBAL_SCOPE, allow_global=True)) self.assertEqual(global_closure, [GLOBAL_SCOPE]) global_closure_excluded = list(all_enclosing_scopes(GLOBAL_SCOPE, allow_global=False)) self.assertEqual(global_closure_excluded, []) base_scope = 'scope' base_scope_closure = list(all_enclosing_scopes(base_scope)) self.assertEqual(base_scope_closure, [base_scope, GLOBAL_SCOPE]) subscope = 'subscope' compound_scope = '{}.{}'.format(base_scope, subscope) compound_scope_closure = list(all_enclosing_scopes(compound_scope)) self.assertEqual(compound_scope_closure, [compound_scope, base_scope, GLOBAL_SCOPE]) compound_scope_closure_no_global = list(all_enclosing_scopes(compound_scope, allow_global=False)) self.assertEqual(compound_scope_closure_no_global, [compound_scope, base_scope])
def get_fingerprintable_for_scope(self, bottom_scope, include_passthru=False, fingerprint_key=None, invert=False): """Returns a list of fingerprintable (option type, option value) pairs for the given scope. Fingerprintable options are options registered via a "fingerprint=True" kwarg. This flag can be parameterized with `fingerprint_key` for special cases. This method also searches enclosing options scopes of `bottom_scope` to determine the set of fingerprintable pairs. :param str bottom_scope: The scope to gather fingerprintable options for. :param bool include_passthru: Whether to include passthru args captured by `bottom_scope` in the fingerprintable options. :param string fingerprint_key: The option kwarg to match against (defaults to 'fingerprint'). :param bool invert: Whether or not to invert the boolean check for the fingerprint_key value. :API: public """ fingerprint_key = fingerprint_key or 'fingerprint' fingerprint_default = bool(invert) pairs = [] if include_passthru: # Passthru args can only be sent to outermost scopes so we gather them once here up-front. passthru_args = self.passthru_args_for_scope(bottom_scope) # NB: We can't sort passthru args, the underlying consumer may be order-sensitive. pairs.extend((str, pass_arg) for pass_arg in passthru_args) # Note that we iterate over options registered at `bottom_scope` and at all # enclosing scopes, since option-using code can read those values indirectly # via its own OptionValueContainer, so they can affect that code's output. for registration_scope in all_enclosing_scopes(bottom_scope): parser = self._parser_hierarchy.get_parser_by_scope(registration_scope) # Sort the arguments, so that the fingerprint is consistent. for (_, kwargs) in sorted(parser.option_registrations_iter()): if kwargs.get('recursive', False) and not kwargs.get('recursive_root', False): continue # We only need to fprint recursive options once. if kwargs.get(fingerprint_key, fingerprint_default) is not True: continue # Note that we read the value from scope, even if the registration was on an enclosing # scope, to get the right value for recursive options (and because this mirrors what # option-using code does). val = self.for_scope(bottom_scope)[kwargs['dest']] # If we have a list then we delegate to the fingerprinting implementation of the members. if is_list_option(kwargs): val_type = kwargs.get('member_type', str) else: val_type = kwargs.get('type', str) pairs.append((val_type, val)) return pairs
def test_valid_invalid_scope(self): """Scopes with dashes or underscores are treated as a single component, and scopes with empty components raise an InvalidScopeError.""" base_dashed_scope = 'base-scope' self.assertEqual(enclosing_scope(base_dashed_scope), GLOBAL_SCOPE) subscope_underscore = 'sub_scope' self.assertEqual(enclosing_scope(subscope_underscore), GLOBAL_SCOPE) compound_scope = '{}.{}'.format(base_dashed_scope, subscope_underscore) self.assertEqual(enclosing_scope(compound_scope), base_dashed_scope) self.assertEqual(list(all_enclosing_scopes(compound_scope)), [ compound_scope, base_dashed_scope, GLOBAL_SCOPE, ]) invalid_scope = 'a.b..c.d' with self.assertRaises(InvalidScopeError): enclosing_scope(invalid_scope) with self.assertRaises(InvalidScopeError): # need to bounce to list to get it to error since this is a generator list(all_enclosing_scopes(invalid_scope))