def _parameterized_type(self, base_type, parameters): """Return a parameterized type.""" if self._is_literal_base_type(base_type): return types.pytd_literal(parameters) elif any(isinstance(p, types.Constant) for p in parameters): parameters = ", ".join( p.repr_str() if isinstance(p, types.Constant) else "_" for p in parameters) raise ParseError( "%s[%s] not supported" % (pytd_utils.Print(base_type), parameters)) elif pytdgen.is_any(base_type): return pytd.AnythingType() elif len(parameters) == 2 and parameters[-1] is self.ELLIPSIS and ( not self._is_callable_base_type(base_type)): element_type = parameters[0] if element_type is self.ELLIPSIS: raise ParseError("[..., ...] not supported") return pytd.GenericType(base_type=base_type, parameters=(element_type,)) else: parameters = tuple(pytd.AnythingType() if p is self.ELLIPSIS else p for p in parameters) if self._is_tuple_base_type(base_type): return pytdgen.heterogeneous_tuple(base_type, parameters) elif self._is_callable_base_type(base_type): return pytdgen.pytd_callable(base_type, parameters) else: assert parameters return pytd.GenericType(base_type=base_type, parameters=parameters)
def get_bases( bases: List[pytd.Type]) -> Tuple[List[pytd_node.Node], Optional[int]]: """Collect base classes and namedtuple index.""" bases_out = [] namedtuple_index = None for i, p in enumerate(bases): if p.name and pytd_utils.MatchesFullName(p, _PROTOCOL_ALIASES): bases_out.append(pytd.NamedType("typing.Protocol")) if isinstance(p, pytd.GenericType): # From PEP 544: "`Protocol[T, S, ...]` is allowed as a shorthand for # `Protocol, Generic[T, S, ...]`." # https://www.python.org/dev/peps/pep-0544/#generic-protocols bases_out.append( p.Replace(base_type=pytd.NamedType("typing.Generic"))) elif isinstance(p, pytd.NamedType) and p.name == "typing.NamedTuple": if namedtuple_index is not None: raise ParseError( "cannot inherit from bare NamedTuple more than once") namedtuple_index = i bases_out.append(p) elif isinstance(p, pytd.Type): bases_out.append(p) else: msg = f"Unexpected class base: {p}" raise ParseError(msg) return bases_out, namedtuple_index
def _split_definitions(defs: List[Any]): """Return [constants], [functions] given a mixed list of definitions.""" constants = [] functions = [] aliases = [] slots = None classes = [] for d in defs: if isinstance(d, pytd.Class): classes.append(d) elif isinstance(d, pytd.Constant): if d.name == "__slots__": pass # ignore definitions of __slots__ as a type else: constants.append(d) elif isinstance(d, function.NameAndSig): functions.append(d) elif isinstance(d, pytd.Alias): aliases.append(d) elif isinstance(d, types.SlotDecl): if slots is not None: raise ParseError("Duplicate __slots__ declaration") slots = d.slots elif isinstance(d, types.Ellipsis): pass elif isinstance(d, ast3.Expr): raise ParseError("Unexpected expression").at(d) else: msg = "Unexpected definition" lineno = None if isinstance(d, ast3.AST): lineno = getattr(d, "lineno", None) raise ParseError(msg, line=lineno) return constants, functions, aliases, slots, classes
def get_parents( bases: List[ast3.AST]) -> Tuple[List[pytd_node.Node], Optional[int]]: """Collect base classes and namedtuple index.""" parents = [] namedtuple_index = None for i, p in enumerate(bases): if _is_parameterized_protocol(p): # From PEP 544: "`Protocol[T, S, ...]` is allowed as a shorthand for # `Protocol, Generic[T, S, ...]`." # https://www.python.org/dev/peps/pep-0544/#generic-protocols parents.append(p.base_type) parents.append( p.Replace(base_type=pytd.NamedType("typing.Generic"))) elif isinstance(p, pytd.NamedType) and p.name == "typing.NamedTuple": if namedtuple_index is not None: raise ParseError( "cannot inherit from bare NamedTuple more than once") namedtuple_index = i parents.append(p) elif isinstance(p, pytd.Type): parents.append(p) else: msg = "Unexpected class base:" + p raise ParseError(msg) return parents, namedtuple_index
def from_function(cls, function: ast3.AST, is_async: bool) -> "NameAndSig": """Constructor from an ast.FunctionDef node.""" name = function.name # decorators decorators = set(function.decorator_list) abstracts = {"abstractmethod", "abc.abstractmethod"} coroutines = {"coroutine", "asyncio.coroutine", "coroutines.coroutine"} overload = {"overload"} ignored = {"type_check_only"} is_abstract = bool(decorators & abstracts) is_coroutine = bool(decorators & coroutines) is_overload = bool(decorators & overload) decorators -= abstracts decorators -= coroutines decorators -= overload decorators -= ignored # TODO(mdemello): do we need this limitation? if len(decorators) > 1: raise ParseError("Too many decorators for %s" % name) decorator, = decorators if decorators else (None,) exceptions = [] mutators = [] for i, x in enumerate(function.body): if isinstance(x, types.Raise): exceptions.append(x.exception) elif isinstance(x, Mutator): mutators.append(x) elif isinstance(x, types.Ellipsis): pass elif (isinstance(x, ast3.Expr) and isinstance(x.value, ast3.Str) and i == 0): # docstring pass else: msg = textwrap.dedent(""" Unexpected statement in function body. Only `raise` statements and type mutations are valid """).lstrip() if isinstance(x, ast3.AST): raise ParseError(msg).at(x) else: raise ParseError(msg) # exceptions sig = _pytd_signature(function, is_async, exceptions=exceptions) # mutators for mutator in mutators: try: sig = sig.Visit(mutator) except NotImplementedError as e: raise ParseError(utils.message(e)) from e if not mutator.successful: raise ParseError("No parameter named %s" % mutator.name) return cls(name, sig, decorator, is_abstract, is_coroutine, is_overload)
def _parameterized_type(self, base_type: Any, parameters): """Return a parameterized type.""" if self._matches_named_type(base_type, _LITERAL_TYPES): return pytd_literal(parameters) elif self._matches_named_type(base_type, _ANNOTATED_TYPES): return pytd_annotated(parameters) elif self._matches_named_type(base_type, _FINAL_TYPES): typ, = parameters return pytd.GenericType(pytd.NamedType("typing.Final"), (typ, )) elif self._matches_named_type(base_type, _TYPEGUARD_TYPES): # We do not yet support PEP 647, User-Defined Type Guards. To avoid # blocking typeshed, convert type guards to plain bools. return pytd.NamedType("bool") elif any(isinstance(p, types.Pyval) for p in parameters): parameters = ", ".join( p.repr_str() if isinstance(p, types.Pyval) else "_" for p in parameters) raise ParseError("%s[%s] not supported" % (pytd_utils.Print(base_type), parameters)) elif pytdgen.is_any(base_type): return pytd.AnythingType() elif len(parameters) == 2 and parameters[-1] is self.ELLIPSIS and ( not self._matches_named_type(base_type, _CALLABLE_TYPES)): element_type = parameters[0] if element_type is self.ELLIPSIS: raise ParseError("[..., ...] not supported") return pytd.GenericType(base_type=base_type, parameters=(element_type, )) else: processed_parameters = [] # We do not yet support PEP 612, Parameter Specification Variables. # To avoid blocking typeshed from adopting this PEP, we convert new # features to approximations that only use supported features. for p in parameters: if p is self.ELLIPSIS: processed = pytd.AnythingType() elif (p in self.param_specs and self._matches_full_name( base_type, "typing.Generic")): # Replacing a ParamSpec with a TypeVar isn't correct, but it'll work # for simple cases in which the filled value is also a ParamSpec. if not any(t.name == p.name for t in self.type_params): self.type_params.append(pytd.TypeParameter(p.name)) processed = p elif (p in self.param_specs or (isinstance(p, pytd.GenericType) and self._matches_full_name(p, _CONCATENATE_TYPES))): processed = pytd.AnythingType() else: processed = p processed_parameters.append(processed) parameters = tuple(processed_parameters) if self._matches_named_type(base_type, _TUPLE_TYPES): return pytdgen.heterogeneous_tuple(base_type, parameters) elif self._matches_named_type(base_type, _CALLABLE_TYPES): return pytdgen.pytd_callable(base_type, parameters) else: assert parameters return pytd.GenericType(base_type=base_type, parameters=parameters)
def build_type_decl_unit(self, defs) -> pytd.TypeDeclUnit: """Return a pytd.TypeDeclUnit for the given defs (plus parser state).""" # defs contains both constant and function definitions. constants, functions, aliases, slots, classes = _split_definitions(defs) assert not slots # slots aren't allowed on the module level # TODO(mdemello): alias/constant handling is broken in some weird manner. # assert not aliases # We handle top-level aliases in add_alias_or_constant # constants.extend(self.constants) if self.module_info.module_name == "builtins": constants.extend(types.builtin_keyword_constants()) generated_classes = sum(self.generated_classes.values(), []) classes = generated_classes + classes functions = function.merge_method_signatures(functions) name_to_class = {c.name: c for c in classes} name_to_constant = {c.name: c for c in constants} aliases = [] for a in self.aliases.values(): t = _maybe_resolve_alias(a, name_to_class, name_to_constant) if t is None: continue elif isinstance(t, pytd.Function): functions.append(t) elif isinstance(t, pytd.Constant): constants.append(t) else: assert isinstance(t, pytd.Alias) aliases.append(t) all_names = ([f.name for f in functions] + [c.name for c in constants] + [c.name for c in self.type_params] + [c.name for c in classes] + [c.name for c in aliases]) duplicates = [name for name, count in collections.Counter(all_names).items() if count >= 2] if duplicates: raise ParseError( "Duplicate top-level identifier(s): " + ", ".join(duplicates)) properties = [x for x in functions if x.kind == pytd.MethodTypes.PROPERTY] if properties: prop_names = ", ".join(p.name for p in properties) raise ParseError( "Module-level functions with property decorators: " + prop_names) return pytd.TypeDeclUnit(name=None, constants=tuple(constants), type_params=tuple(self.type_params), functions=tuple(functions), classes=tuple(classes), aliases=tuple(aliases))
def new_type(self, name: Union[str, pytd_node.Node], parameters: Optional[List[pytd.Type]] = None) -> pytd.Type: """Return the AST for a type. Args: name: The name of the type. parameters: List of type parameters. Returns: A pytd type node. Raises: ParseError: if the wrong number of parameters is supplied for the base_type - e.g., 2 parameters to Optional or no parameters to Union. """ base_type = self.resolve_type(name) for p in self.param_specs: if base_type.name.startswith(f"{p.name}."): _, attr = base_type.name.split(".", 1) if attr not in ("args", "kwargs"): raise ParseError( f"Unrecognized ParamSpec attribute: {attr}") # We do not yet support typing.ParamSpec, so replace references to its # args and kwargs attributes with Any. return pytd.AnythingType() if not isinstance(base_type, pytd.NamedType): # We assume that all type parameters have been defined. Since pytype # orders type parameters to appear before classes and functions, this # assumption is generally safe. AnyStr is special-cased because imported # type parameters aren't recognized. type_params = self.type_params + [ pytd.TypeParameter("typing.AnyStr") ] base_type = base_type.Visit(_InsertTypeParameters(type_params)) try: resolved_type = visitors.MaybeSubstituteParameters( base_type, parameters) except ValueError as e: raise ParseError(str(e)) from e if resolved_type: return resolved_type if parameters is not None: if (len(parameters) > 1 and isinstance(base_type, pytd.NamedType) and base_type.name == "typing.Optional"): raise ParseError("Too many options to %s" % base_type.name) return self._parameterized_type(base_type, parameters) else: if (isinstance(base_type, pytd.NamedType) and base_type.name in _TYPING_SETS): raise ParseError("Missing options to %s" % base_type.name) return base_type
def _check_module_functions(functions): """Validate top-level module functions.""" # module.__getattr__ should have a unique signature g = [f for f in functions if f.name == "__getattr__"] if g and len(g[0].signatures) > 1: raise ParseError("Multiple signatures for module __getattr__") # module-level functions cannot be properties properties = [x for x in functions if x.kind == pytd.MethodKind.PROPERTY] if properties: prop_names = ", ".join(p.name for p in properties) raise ParseError("Module-level functions with property decorators: " + prop_names)
def _parameterized_type(self, base_type, parameters): """Return a parameterized type.""" if self._matches_named_type(base_type, _LITERAL_TYPES): return types.pytd_literal(parameters) elif self._matches_named_type(base_type, _ANNOTATED_TYPES): return types.pytd_annotated(parameters) elif self._matches_named_type(base_type, _TYPEGUARD_TYPES): # We do not yet support PEP 647, User-Defined Type Guards. To avoid # blocking typeshed, convert type guards to plain bools. return pytd.NamedType("bool") elif any(isinstance(p, types.Constant) for p in parameters): parameters = ", ".join( p.repr_str() if isinstance(p, types.Constant) else "_" for p in parameters) raise ParseError("%s[%s] not supported" % (pytd_utils.Print(base_type), parameters)) elif pytdgen.is_any(base_type): return pytd.AnythingType() elif len(parameters) == 2 and parameters[-1] is self.ELLIPSIS and ( not self._matches_named_type(base_type, _CALLABLE_TYPES)): element_type = parameters[0] if element_type is self.ELLIPSIS: raise ParseError("[..., ...] not supported") return pytd.GenericType(base_type=base_type, parameters=(element_type, )) else: parameters = tuple(pytd.AnythingType() if p is self.ELLIPSIS else p for p in parameters) if self._matches_named_type(base_type, _TUPLE_TYPES): return pytdgen.heterogeneous_tuple(base_type, parameters) elif self._matches_named_type(base_type, _CALLABLE_TYPES): callable_parameters = [] for p in parameters: # We do not yet support PEP 612, Parameter Specification Variables. # To avoid blocking typeshed from adopting this PEP, we convert new # features to Any. if p in self.param_specs or (isinstance( p, pytd.GenericType) and self._matches_full_name( p, _CONCATENATE_TYPES)): callable_parameters.append(pytd.AnythingType()) else: callable_parameters.append(p) return pytdgen.pytd_callable(base_type, tuple(callable_parameters)) else: assert parameters return pytd.GenericType(base_type=base_type, parameters=parameters)
def fail(self, name=None): if name: msg = f"Unsupported condition: '{name}'. " else: msg = "Unsupported condition. " msg += "Supported checks are sys.platform and sys.version_info" raise ParseError(msg)
def visit_BoolOp(self, node): if isinstance(node.op, ast3.Or): return any(node.values) elif isinstance(node.op, ast3.And): return all(node.values) else: raise ParseError("Unexpected boolean operator: " + node.op)
def pytd_annotated(parameters: List[Any]) -> pytd.Type: """Create a pytd.Annotated.""" if len(parameters) < 2: raise ParseError("typing.Annotated takes at least two parameters: " "Annotated[type, annotation, ...].") typ, *annotations = parameters annotations = tuple(map(_convert_annotated, annotations)) return pytd.Annotated(typ, annotations)
def get_metaclass(keywords: List[ast3.AST], parents: List[pytd_node.Node]): """Scan keywords for a metaclass.""" for k in keywords: keyword, value = k.arg, k.value if keyword not in ("metaclass", "total"): raise ParseError("Unexpected classdef kwarg %r" % keyword) elif keyword == "total" and not any( isinstance(parent, pytd.NamedType) and pytd_utils.MatchesFullName(parent, _TYPED_DICT_ALIASES) for parent in parents): raise ParseError( "'total' allowed as classdef kwarg only for TypedDict subclasses" ) if keyword == "metaclass": return value return None
def add_param_spec(self, name, paramspec): if name != paramspec.name: raise ParseError("ParamSpec name needs to be %r (not %r)" % (paramspec.name, name)) # ParamSpec should probably be represented with its own pytd class, like # TypeVar. This is just a quick, hacky way for us to keep track of which # names refer to ParamSpecs so we can replace them with Any in # _parameterized_type(). self.param_specs.append(pytd.NamedType(name))
def EnterParameter(self, node): if isinstance(node.mutated_type, pytd.GenericType): params = self._GetTypeParameters(node.mutated_type) extra = params - self.type_params_in_scope[-1] if extra: fn = pytd_utils.Print(self.current_function) msg = "Type parameter(s) {%s} not in scope in\n\n%s" % ( ", ".join(sorted(extra)), fn) raise ParseError(msg)
def _parameterized_type(self, base_type, parameters): """Return a parameterized type.""" if self._is_literal_base_type(base_type): return types.pytd_literal(parameters) elif any(isinstance(p, types.Constant) for p in parameters): parameters = ", ".join( p.repr_str() if isinstance(p, types.Constant) else "_" for p in parameters) raise ParseError("%s[%s] not supported" % (pytd_utils.Print(base_type), parameters)) elif pytdgen.is_any(base_type): return pytd.AnythingType() elif len(parameters) == 2 and parameters[-1] is self.ELLIPSIS and ( not self._is_callable_base_type(base_type)): element_type = parameters[0] if element_type is self.ELLIPSIS: raise ParseError("[..., ...] not supported") return pytd.GenericType(base_type=base_type, parameters=(element_type, )) else: parameters = tuple(pytd.AnythingType() if p is self.ELLIPSIS else p for p in parameters) if self._is_tuple_base_type(base_type): return pytdgen.heterogeneous_tuple(base_type, parameters) elif self._is_callable_base_type(base_type): callable_parameters = [] for p in parameters: # We do not yet support PEP 612, Parameter Specification Variables. # To avoid blocking typeshed from adopting this PEP, we convert new # features to Any. if p in self.param_specs or (isinstance( p, pytd.GenericType) and self._matches_full_name( p, ("typing.Concatenate", "typing_extensions.Concatenate"))): callable_parameters.append(pytd.AnythingType()) else: callable_parameters.append(p) return pytdgen.pytd_callable(base_type, tuple(callable_parameters)) else: assert parameters return pytd.GenericType(base_type=base_type, parameters=parameters)
def check_for_duplicate_defs(methods, constants, aliases) -> None: """Check a class's list of definitions for duplicates.""" all_names = (list(set(f.name for f in methods)) + [c.name for c in constants] + [a.name for a in aliases]) duplicates = [ name for name, count in collections.Counter(all_names).items() if count >= 2 ] if duplicates: raise ParseError("Duplicate class-level identifier(s): " + ", ".join(duplicates))
def _convert_annotated(x): """Convert everything to a string to store it in pytd.Annotated.""" if isinstance(x, types.Pyval): return x.repr_str() elif isinstance(x, dict): return metadata.to_string(x) elif isinstance(x, tuple): fn, posargs, kwargs = x return metadata.call_to_annotation(fn, posargs=posargs, kwargs=kwargs) else: raise ParseError(f"Cannot convert metadata {x}")
def _qualify_name_with_special_dir(self, orig_name): """Handle the case of '.' and '..' as package names.""" if "__PACKAGE__." in orig_name: # Generated from "from . import foo" - see parser.yy prefix, _, name = orig_name.partition("__PACKAGE__.") if prefix: raise ParseError(f"Cannot resolve import: {orig_name}") return f"{self.package_name}.{name}" elif "__PARENT__." in orig_name: # Generated from "from .. import foo" - see parser.yy prefix, _, name = orig_name.partition("__PARENT__.") if prefix: raise ParseError(f"Cannot resolve import: {orig_name}") if not self.parent_name: raise ParseError( f"Cannot resolve relative import ..: Package {self.package_name} " "has no parent") return f"{self.parent_name}.{name}" else: return None
def add_type_var(self, name, typevar): """Add a type variable, <name> = TypeVar(<name_arg>, <args>).""" if name != typevar.name: raise ParseError("TypeVar name needs to be %r (not %r)" % ( typevar.name, name)) bound = typevar.bound if isinstance(bound, str): bound = pytd.NamedType(bound) constraints = tuple(typevar.constraints) if typevar.constraints else () self.type_params.append(pytd.TypeParameter( name=name, constraints=constraints, bound=bound))
def pytd_literal(parameters: List[Any]) -> pytd.Type: """Create a pytd.Literal.""" literal_parameters = [] for p in parameters: if pytdgen.is_none(p): literal_parameters.append(p) elif isinstance(p, pytd.NamedType): # TODO(b/173742489): support enums. literal_parameters.append(pytd.AnythingType()) elif isinstance(p, types.Pyval): literal_parameters.append(p.to_pytd_literal()) elif isinstance(p, pytd.Literal): literal_parameters.append(p) elif isinstance(p, pytd.UnionType): for t in p.type_list: if isinstance(t, pytd.Literal): literal_parameters.append(t) else: raise ParseError(f"Literal[{t}] not supported") else: raise ParseError(f"Literal[{p}] not supported") return pytd_utils.JoinTypes(literal_parameters)
def get_metaclass(keywords: List[ast3.keyword]): """Scan keywords for a metaclass.""" for k in keywords: keyword, value = k.arg, k.value if keyword not in ("metaclass", "total"): raise ParseError(f"Unexpected classdef kwarg {keyword!r}") # TODO(rechen): We should store the "total" value instead of throwing it # away and validate in load_pytd that "total" is passed only to TypedDict # subclasses. We can't do the validation here because external types need to # be resolved first. if keyword == "metaclass": return value return None
def qualify_name(self, orig_name): """Qualify an import name.""" if not self.package_name: return orig_name rel_name = self._qualify_name_with_special_dir(orig_name) if rel_name: return rel_name if orig_name.startswith("."): name = module_utils.get_absolute_name(self.package_name, orig_name) if name is None: raise ParseError( f"Cannot resolve relative import {orig_name.rsplit('.', 1)[0]}" ) return name return orig_name
def add_alias_or_constant(self, alias_or_constant): """Add an alias or constant. Args: alias_or_constant: the top-level definition to add. Raises: ParseError: For an invalid __slots__ declaration. """ if isinstance(alias_or_constant, pytd.Constant): self.constants.append(alias_or_constant) elif isinstance(alias_or_constant, types.SlotDecl): raise ParseError("__slots__ only allowed on the class level") elif isinstance(alias_or_constant, pytd.Alias): name, value = alias_or_constant.name, alias_or_constant.type self.type_map[name] = value self.aliases[name] = alias_or_constant else: assert False, "Unknown type of assignment"
def get_decorators(decorators: List[str], type_map: Dict[str, pytd_node.Node]): """Process a class decorator list.""" # Drop the @type_check_only decorator from classes # TODO(mdemello): Workaround for the bug that typing.foo class decorators # don't add the import, since typing.type_check_only is the only one. decorators = [x for x in decorators if x != "type_check_only"] # Check for some function/method-only decorators nonclass = {"property", "classmethod", "staticmethod", "overload"} unsupported_decorators = set(decorators) & nonclass if unsupported_decorators: raise ParseError("Unsupported class decorators: %s" % ", ".join(unsupported_decorators)) # Convert decorators to named types. These are wrapped as aliases because we # otherwise do not allow referencing functions as types. return [ pytd.Alias(d, type_map.get(d) or pytd.NamedType(d)) for d in decorators ]
def _eval_comparison(self, ident, op, value) -> bool: """Evaluate a comparison and return a bool. Args: ident: A tuple of a dotted name string and an optional __getitem__ key (int or slice). op: One of the comparison operator strings in cmp_slots.COMPARES. value: Either a string, an integer, or a tuple of integers. Returns: The boolean result of the comparison. Raises: ParseError: If the comparison cannot be evaluated. """ name, key = ident if name == "sys.version_info": if key is None: key = slice(None, None, None) assert isinstance(key, (int, slice)) if isinstance(key, int) and not isinstance(value, int): raise ParseError( "an element of sys.version_info must be compared to an integer" ) if isinstance(key, slice) and not _is_int_tuple(value): raise ParseError( "sys.version_info must be compared to a tuple of integers") try: actual = self._version[key] except IndexError as e: raise ParseError(utils.message(e)) from e if isinstance(key, slice): actual = _three_tuple(actual) value = _three_tuple(value) elif name == "sys.platform": if not isinstance(value, str): raise ParseError("sys.platform must be compared to a string") valid_cmps = (cmp_slots.EQ, cmp_slots.NE) if op not in valid_cmps: raise ParseError( "sys.platform must be compared using %s or %s" % valid_cmps) actual = self._platform else: raise ParseError("Unsupported condition: '%s'." % name) return cmp_slots.COMPARES[op](actual, value)
def leave(self, node): try: return super().leave(node) except Exception as e: # pylint: disable=broad-except raise ParseError.from_exc(e).at(node, self.filename, self.src_code)
def visit_UnaryOp(self, node): if isinstance(node.op, ast3.USub) and isinstance(node.operand, int): return -node.operand else: raise ParseError("Unexpected unary operator: %s" % node.op)
def visit_UnaryOp(self, node): if isinstance(node.op, ast3.USub): if isinstance(node.operand, types.Pyval): return node.operand.negated() raise ParseError(f"Unexpected unary operator: {node.op}")