from__future__importannotationsimportdataclassesimportsysimporttypingast# pass slots=True on 3.10+# it's not strictly necessary, but it improves performanceifsys.version_info>=(3,10):_add_dataclass_kwargs:dict[str,bool]={"slots":True}else:_add_dataclass_kwargs:dict[str,bool]={}
[docs]@dataclasses.dataclass(frozen=True,repr=False,**_add_dataclass_kwargs)classScope:""" A scope object is a representation of a scope and its dynamic dependencies (other scopes). A scope may be optional (also referred to as "atomically revocable"). An optional scope can be revoked without revoking consent for other scopes which were granted at the same time. Scopes are immutable, and provide several evolver methods which produce new Scopes. In particular, ``with_dependency`` and ``with_dependencies`` create new scopes with added dependencies. ``str(Scope(...))`` produces a valid scope string for use in various methods. :param scope_string: The string which will be used as the basis for this Scope :param optional: The scope may be marked as optional. This means that the scope can be declined by the user without declining consent for other scopes. """scope_string:stroptional:bool=dataclasses.field(default=False)dependencies:tuple[Scope,...]=dataclasses.field(default=())def__post_init__(self,)->None:ifany(cinself.scope_stringforcin"[]* "):raiseValueError("Scope instances may not contain the special characters '[]* '. ""Use Scope.parse instead.")
[docs]@classmethoddefparse(cls,scope_string:str)->Scope:""" Deserialize a scope string to a scope object. This is the special case of parsing in which exactly one scope must be returned by the parse. If more than one scope is returned by the parse, a ``ValueError`` will be raised. :param scope_string: The string to parse """# deferred import because ScopeParser depends on Scope, but Scope.parse# is a wrapper over ScopeParser.parse()from.parserimportScopeParserdata=ScopeParser.parse(scope_string)iflen(data)!=1:raiseValueError("`Scope.parse()` did not get exactly one scope. "f"Instead got data={data}")returndata[0]
[docs]defwith_dependency(self,other_scope:Scope)->Scope:""" Create a new scope with a dependency. The dependent scope relationship will be stored in the Scope and will be evident in its string representation. :param other_scope: The scope upon which the current scope depends. """returndataclasses.replace(self,dependencies=self.dependencies+(other_scope,))
[docs]defwith_dependencies(self,other_scopes:t.Iterable[Scope])->Scope:""" Create a new scope with added dependencies. The dependent scope relationships will be stored in the Scope and will be evident in its string representation. :param other_scopes: The scopes upon which the current scope depends. """returndataclasses.replace(self,dependencies=self.dependencies+tuple(other_scopes))
[docs]defwith_optional(self,optional:bool)->Scope:""" Create a new scope with a different 'optional' value. :param optional: Whether or not the scope is optional. """returndataclasses.replace(self,optional=optional)