diff -r e21b51a3d990 -r e143a7e7254b src/eric7/Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py --- a/src/eric7/Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py Sat Oct 01 19:42:50 2022 +0200 +++ b/src/eric7/Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py Sat Oct 01 20:06:27 2022 +0200 @@ -12,6 +12,7 @@ Also, it models the Bindings and Scopes. """ import __future__ +import builtins import ast import bisect import collections @@ -26,70 +27,22 @@ from . import messages -PY2 = sys.version_info < (3, 0) -PY35_PLUS = sys.version_info >= (3, 5) # Python 3.5 and above -PY36_PLUS = sys.version_info >= (3, 6) # Python 3.6 and above PY38_PLUS = sys.version_info >= (3, 8) -try: - sys.pypy_version_info - PYPY = True -except AttributeError: - PYPY = False +PYPY = hasattr(sys, 'pypy_version_info') -builtin_vars = dir(__import__('__builtin__' if PY2 else 'builtins')) +builtin_vars = dir(builtins) parse_format_string = string.Formatter().parse -if PY2: - tokenize_tokenize = tokenize.generate_tokens -else: - tokenize_tokenize = tokenize.tokenize - -if PY2: - def getNodeType(node_class): - # workaround str.upper() which is locale-dependent - return str(unicode(node_class.__name__).upper()) # __IGNORE_WARNING__ - - def get_raise_argument(node): - return node.type - -else: - def getNodeType(node_class): - return node_class.__name__.upper() - - def get_raise_argument(node): - return node.exc - - # Silence `pyflakes` from reporting `undefined name 'unicode'` in Python 3. - unicode = str -# Python >= 3.3 uses ast.Try instead of (ast.TryExcept + ast.TryFinally) -if PY2: - def getAlternatives(n): - if isinstance(n, (ast.If, ast.TryFinally)): - return [n.body] - if isinstance(n, ast.TryExcept): - return [n.body + n.orelse] + [[hdl] for hdl in n.handlers] -else: - def getAlternatives(n): - if isinstance(n, ast.If): - return [n.body] - if isinstance(n, ast.Try): - return [n.body + n.orelse] + [[hdl] for hdl in n.handlers] +def getAlternatives(n): + if isinstance(n, ast.If): + return [n.body] + if isinstance(n, ast.Try): + return [n.body + n.orelse] + [[hdl] for hdl in n.handlers] -if PY35_PLUS: - FOR_TYPES = (ast.For, ast.AsyncFor) - LOOP_TYPES = (ast.While, ast.For, ast.AsyncFor) - FUNCTION_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) -else: - FOR_TYPES = (ast.For,) - LOOP_TYPES = (ast.While, ast.For) - FUNCTION_TYPES = (ast.FunctionDef,) -if PY36_PLUS: - ANNASSIGN_TYPES = (ast.AnnAssign,) -else: - ANNASSIGN_TYPES = () +FOR_TYPES = (ast.For, ast.AsyncFor) if PY38_PLUS: def _is_singleton(node): # type: (ast.AST) -> bool @@ -97,15 +50,9 @@ isinstance(node, ast.Constant) and isinstance(node.value, (bool, type(Ellipsis), type(None))) ) -elif not PY2: - def _is_singleton(node): # type: (ast.AST) -> bool - return isinstance(node, (ast.NameConstant, ast.Ellipsis)) else: def _is_singleton(node): # type: (ast.AST) -> bool - return ( - isinstance(node, ast.Name) and - node.id in {'True', 'False', 'Ellipsis', 'None'} - ) + return isinstance(node, (ast.NameConstant, ast.Ellipsis)) def _is_tuple_constant(node): # type: (ast.AST) -> bool @@ -119,13 +66,9 @@ def _is_constant(node): return isinstance(node, ast.Constant) or _is_tuple_constant(node) else: - _const_tps = (ast.Str, ast.Num) - if not PY2: - _const_tps += (ast.Bytes,) - def _is_constant(node): return ( - isinstance(node, _const_tps) or + isinstance(node, (ast.Str, ast.Num, ast.Bytes)) or _is_singleton(node) or _is_tuple_constant(node) ) @@ -135,7 +78,7 @@ return _is_constant(node) and not _is_singleton(node) -def _is_name_or_attr(node, name): # type: (ast.Ast, str) -> bool +def _is_name_or_attr(node, name): # type: (ast.AST, str) -> bool return ( (isinstance(node, ast.Name) and node.id == name) or (isinstance(node, ast.Attribute) and node.attr == name) @@ -147,7 +90,7 @@ # https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L1408-L1413 ASCII_NON_ALNUM = ''.join([chr(i) for i in range(128) if not chr(i).isalnum()]) TYPE_IGNORE_RE = re.compile( - TYPE_COMMENT_RE.pattern + r'ignore([{}]|$)'.format(ASCII_NON_ALNUM)) + TYPE_COMMENT_RE.pattern + fr'ignore([{ASCII_NON_ALNUM}]|$)') # https://github.com/python/typed_ast/blob/1.4.0/ast27/Grammar/Grammar#L147 TYPE_FUNC_RE = re.compile(r'^(\(.*?\))\s*->\s*(.*)$') @@ -162,20 +105,18 @@ def _must_match(regex, string, pos): - # type: (Pattern[str], str, int) -> Match[str] match = regex.match(string, pos) assert match is not None return match -def parse_percent_format(s): # type: (str) -> Tuple[PercentFormat, ...] +def parse_percent_format(s): """Parses the string component of a `'...' % ...` format call Copied from https://github.com/asottile/pyupgrade at v1.20.1 """ def _parse_inner(): - # type: () -> Generator[PercentFormat, None, None] string_start = 0 string_end = 0 in_fmt = False @@ -195,7 +136,7 @@ else: key_match = MAPPING_KEY_RE.match(s, i) if key_match: - key = key_match.group(1) # type: Optional[str] + key = key_match.group(1) i = key_match.end() else: key = None @@ -306,8 +247,7 @@ result.name, result, ) - elif (not PY2) and isinstance(item, ast.NameConstant): - # None, True, False are nameconstants in python3, but names in 2 + elif isinstance(item, ast.NameConstant): return item.value else: return UnhandledKeyType() @@ -317,7 +257,7 @@ return isinstance(node, ast.Name) and getNodeName(node) == 'NotImplemented' -class Binding(object): +class Binding: """ Represents the binding of a value to a name. @@ -338,10 +278,12 @@ return self.name def __repr__(self): - return '<%s object %r from line %r at 0x%x>' % (self.__class__.__name__, - self.name, - self.source.lineno, - id(self)) + return '<{} object {!r} from line {!r} at 0x{:x}>'.format( + self.__class__.__name__, + self.name, + self.source.lineno, + id(self), + ) def redefines(self, other): return isinstance(other, Definition) and self.name == other.name @@ -360,18 +302,20 @@ super().__init__(name, None) def __repr__(self): - return '<%s object %r at 0x%x>' % (self.__class__.__name__, - self.name, - id(self)) + return '<{} object {!r} at 0x{:x}>'.format( + self.__class__.__name__, + self.name, + id(self) + ) -class UnhandledKeyType(object): +class UnhandledKeyType: """ A dictionary key of a type that we cannot or do not check for duplicates. """ -class VariableKey(object): +class VariableKey: """ A dictionary key which is a variable. @@ -418,7 +362,7 @@ def source_statement(self): """Generate a source statement equivalent to the import.""" if self._has_alias(): - return 'import %s as %s' % (self.fullName, self.name) + return f'import {self.fullName} as {self.name}' else: return 'import %s' % self.fullName @@ -490,11 +434,9 @@ @property def source_statement(self): if self.real_name != self.name: - return 'from %s import %s as %s' % (self.module, - self.real_name, - self.name) + return f'from {self.module} import {self.real_name} as {self.name}' else: - return 'from %s import %s' % (self.module, self.name) + return f'from {self.module} import {self.name}' class StarImportation(Importation): @@ -547,6 +489,12 @@ """ +class NamedExprAssignment(Assignment): + """ + Represents binding a name with an assignment expression. + """ + + class Annotation(Binding): """ Represents binding a name to a type without an associated value. @@ -623,7 +571,7 @@ def __repr__(self): scope_cls = self.__class__.__name__ - return '<%s at 0x%x %s>' % (scope_cls, id(self), dict.__repr__(self)) + return f'<{scope_cls} at 0x{id(self):x} {dict.__repr__(self)}>' class ClassScope(Scope): @@ -674,7 +622,7 @@ """Scope for a doctest.""" -class DummyNode(object): +class DummyNode: """Used in place of an `ast.AST` to set error message positions""" def __init__(self, lineno, col_offset): self.lineno = lineno @@ -687,10 +635,7 @@ # Globally defined names which are not attributes of the builtins module, or # are only present on some platforms. -_MAGIC_GLOBALS = ['__file__', '__builtins__', 'WindowsError'] -# module scope annotation will store in `__annotations__`, see also PEP 526. -if PY36_PLUS: - _MAGIC_GLOBALS.append('__annotations__') +_MAGIC_GLOBALS = ['__file__', '__builtins__', '__annotations__', 'WindowsError'] def getNodeName(node): @@ -773,7 +718,7 @@ def is_typing_overload(value, scope_stack): return ( - isinstance(value.source, FUNCTION_TYPES) and + isinstance(value.source, (ast.FunctionDef, ast.AsyncFunctionDef)) and any( _is_typing(dec, 'overload', scope_stack) for dec in value.source.decorator_list @@ -809,7 +754,7 @@ code = code.encode('UTF-8') lines = iter(code.splitlines(True)) # next(lines, b'') is to prevent an error in pypy3 - return tuple(tokenize_tokenize(lambda: next(lines, b''))) + return tuple(tokenize.tokenize(lambda: next(lines, b''))) class _TypeableVisitor(ast.NodeVisitor): @@ -819,8 +764,8 @@ https://www.python.org/dev/peps/pep-0484/#type-comments """ def __init__(self): - self.typeable_lines = [] # type: List[int] - self.typeable_nodes = {} # type: Dict[int, ast.AST] + self.typeable_lines = [] + self.typeable_nodes = {} def _typeable(self, node): # if there is more than one typeable thing on a line last one wins @@ -860,7 +805,7 @@ return type_comments -class Checker(object): +class Checker: """ I check the cleanliness and sanity of Python code. @@ -877,14 +822,13 @@ ast.Module: ModuleScope, ast.ClassDef: ClassScope, ast.FunctionDef: FunctionScope, + ast.AsyncFunctionDef: FunctionScope, ast.Lambda: FunctionScope, ast.ListComp: GeneratorScope, ast.SetComp: GeneratorScope, ast.GeneratorExp: GeneratorScope, ast.DictComp: GeneratorScope, } - if PY35_PLUS: - _ast_node_scope[ast.AsyncFunctionDef] = FunctionScope nodeDepth = 0 offset = None @@ -1145,16 +1089,13 @@ node, value.name, existing.source) elif scope is self.scope: - if (isinstance(parent_stmt, ast.comprehension) and - not isinstance(self.getParent(existing.source), - (FOR_TYPES, ast.comprehension))): - self.report(messages.RedefinedInListComp, + if ( + (not existing.used and value.redefines(existing)) and + (value.name != '_' or isinstance(existing, Importation)) and + not is_typing_overload(existing, self.scopeStack) + ): + self.report(messages.RedefinedWhileUnused, node, value.name, existing.source) - elif not existing.used and value.redefines(existing): - if value.name != '_' or isinstance(existing, Importation): - if not is_typing_overload(existing, self.scopeStack): - self.report(messages.RedefinedWhileUnused, - node, value.name, existing.source) elif isinstance(existing, Importation) and value.redefines(existing): existing.redefined.append(node) @@ -1166,7 +1107,14 @@ # don't treat annotations as assignments if there is an existing value # in scope if value.name not in self.scope or not isinstance(value, Annotation): - self.scope[value.name] = value + cur_scope_pos = -1 + # As per PEP 572, use scope in which outermost generator is defined + while ( + isinstance(value, NamedExprAssignment) and + isinstance(self.scopeStack[cur_scope_pos], GeneratorScope) + ): + cur_scope_pos -= 1 + self.scopeStack[cur_scope_pos][value.name] = value def _unknown_handler(self, node): # this environment variable configures whether to error on unknown @@ -1180,7 +1128,7 @@ # in the pyflakes testsuite (so more specific handling can be added if # needed). if os.environ.get('PYFLAKES_ERROR_UNKNOWN'): - raise NotImplementedError('Unexpected type: {}'.format(type(node))) + raise NotImplementedError(f'Unexpected type: {type(node)}') else: self.handleChildren(node) @@ -1188,7 +1136,7 @@ try: return self._nodeHandlers[node_class] except KeyError: - nodeType = getNodeType(node_class) + nodeType = node_class.__name__.upper() self._nodeHandlers[node_class] = handler = getattr( self, nodeType, self._unknown_handler, ) @@ -1205,7 +1153,7 @@ # try enclosing function scopes and global scope for scope in self.scopeStack[-1::-1]: if isinstance(scope, ClassScope): - if not PY2 and name == '__class__': + if name == '__class__': return elif in_generators is False: # only generators used in a class scope can access the @@ -1292,16 +1240,23 @@ break parent_stmt = self.getParent(node) - if isinstance(parent_stmt, ANNASSIGN_TYPES) and parent_stmt.value is None: + if isinstance(parent_stmt, ast.AnnAssign) and parent_stmt.value is None: binding = Annotation(name, node) elif isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or ( parent_stmt != node._pyflakes_parent and not self.isLiteralTupleUnpacking(parent_stmt)): binding = Binding(name, node) - elif name == '__all__' and isinstance(self.scope, ModuleScope): + elif ( + name == '__all__' and + isinstance(self.scope, ModuleScope) and + isinstance( + node._pyflakes_parent, + (ast.Assign, ast.AugAssign, ast.AnnAssign) + ) + ): binding = ExportBinding(name, node._pyflakes_parent, self.scope) - elif PY2 and isinstance(getattr(node, 'ctx', None), ast.Param): - binding = Argument(name, self.getScopeNode(node)) + elif PY38_PLUS and isinstance(parent_stmt, ast.NamedExpr): + binding = NamedExprAssignment(name, node) else: binding = Assignment(name, node) self.addBinding(node, binding) @@ -1364,8 +1319,6 @@ parts = (comment,) for part in parts: - if PY2: - part = part.replace('...', 'Ellipsis') self.deferFunction(functools.partial( self.handleStringAnnotation, part, DummyNode(lineno, col_offset), lineno, col_offset, @@ -1428,19 +1381,7 @@ def handleDoctests(self, node): try: - if hasattr(node, 'docstring'): - docstring = node.docstring - - # This is just a reasonable guess. In Python 3.7, docstrings no - # longer have line numbers associated with them. This will be - # incorrect if there are empty lines between the beginning - # of the function and the docstring. - node_lineno = node.lineno - if hasattr(node, 'args'): - node_lineno = max([node_lineno] + - [arg.lineno for arg in node.args.args]) - else: - (docstring, node_lineno) = self.getDocstring(node.body[0]) + (docstring, node_lineno) = self.getDocstring(node.body[0]) examples = docstring and self._getDoctestExamples(docstring) except (ValueError, IndexError): # e.g. line 6 of the docstring for <string> has inconsistent @@ -1459,10 +1400,7 @@ for example in examples: try: tree = ast.parse(example.source, "<doctest>") - except SyntaxError: - e = sys.exc_info()[1] - if PYPY: - e.offset += 1 + except SyntaxError as e: position = (node_lineno + example.lineno + e.lineno, example.indent + 4 + (e.offset or 0)) self.report(messages.DoctestSyntaxError, node, position) @@ -1520,16 +1458,14 @@ pass # "stmt" type nodes - DELETE = PRINT = FOR = ASYNCFOR = WHILE = WITH = WITHITEM = \ - ASYNCWITH = ASYNCWITHITEM = TRYFINALLY = EXEC = \ + DELETE = FOR = ASYNCFOR = WHILE = WITH = WITHITEM = ASYNCWITH = \ EXPR = ASSIGN = handleChildren PASS = ignore # "expr" type nodes - BOOLOP = UNARYOP = SET = \ - REPR = ATTRIBUTE = \ - STARRED = NAMECONSTANT = NAMEDEXPR = handleChildren + BOOLOP = UNARYOP = SET = ATTRIBUTE = STARRED = NAMECONSTANT = \ + NAMEDEXPR = handleChildren def SUBSCRIPT(self, node): if _is_name_or_attr(node.value, 'Literal'): @@ -1576,15 +1512,16 @@ self.report(messages.StringDotFormatInvalidFormat, node, e) return - class state: # py2-compatible `nonlocal` - auto = None - next_auto = 0 + auto = None + next_auto = 0 placeholder_positional = set() placeholder_named = set() def _add_key(fmtkey): """Returns True if there is an error which should early-exit""" + nonlocal auto, next_auto + if fmtkey is None: # end of string or `{` / `}` escapes return False @@ -1597,21 +1534,21 @@ except ValueError: pass else: # fmtkey was an integer - if state.auto is True: + if auto is True: self.report(messages.StringDotFormatMixingAutomatic, node) return True else: - state.auto = False + auto = False if fmtkey == '': - if state.auto is False: + if auto is False: self.report(messages.StringDotFormatMixingAutomatic, node) return True else: - state.auto = True + auto = True - fmtkey = state.next_auto - state.next_auto += 1 + fmtkey = next_auto + next_auto += 1 if isinstance(fmtkey, int): placeholder_positional.add(fmtkey) @@ -1646,15 +1583,9 @@ # bail early if there is *args or **kwargs if ( - # python 2.x *args / **kwargs - getattr(node, 'starargs', None) or - getattr(node, 'kwargs', None) or - # python 3.x *args - any( - isinstance(arg, getattr(ast, 'Starred', ())) - for arg in node.args - ) or - # python 3.x **kwargs + # *args + any(isinstance(arg, ast.Starred) for arg in node.args) or + # **kwargs any(kwd.arg is None for kwd in node.keywords) ): return @@ -1835,7 +1766,7 @@ isinstance(node.right, (ast.List, ast.Tuple)) and # does not have any *splats (py35+ feature) not any( - isinstance(elt, getattr(ast, 'Starred', ())) + isinstance(elt, ast.Starred) for elt in node.right.elts ) ): @@ -1919,7 +1850,7 @@ def RAISE(self, node): self.handleChildren(node) - arg = get_raise_argument(node) + arg = node.exc if isinstance(arg, ast.Call): if is_notimplemented_name_node(arg.func): @@ -2034,9 +1965,7 @@ self.handleChildren(node) self.popScope() - LISTCOMP = handleChildren if PY2 else GENERATOREXP - - DICTCOMP = SETCOMP = GENERATOREXP + LISTCOMP = DICTCOMP = SETCOMP = GENERATOREXP def NAME(self, node): """ @@ -2051,13 +1980,11 @@ self.scope.usesLocals = True elif isinstance(node.ctx, ast.Store): self.handleNodeStore(node) - elif PY2 and isinstance(node.ctx, ast.Param): - self.handleNodeStore(node) elif isinstance(node.ctx, ast.Del): self.handleNodeDelete(node) else: # Unknown context - raise RuntimeError("Got impossible expression context: %r" % (node.ctx,)) + raise RuntimeError(f"Got impossible expression context: {node.ctx!r}") def CONTINUE(self, node): # Walk the tree up until we see a loop (OK), a function or class @@ -2066,7 +1993,7 @@ n = node while hasattr(n, '_pyflakes_parent'): n, n_child = n._pyflakes_parent, n - if isinstance(n, LOOP_TYPES): + if isinstance(n, (ast.While, ast.For, ast.AsyncFor)): # Doesn't apply unless it's in the loop itself if n_child not in n.orelse: return @@ -2125,41 +2052,26 @@ args = [] annotations = [] - if PY2: - def addArgs(arglist): - for arg in arglist: - if isinstance(arg, ast.Tuple): - addArgs(arg.elts) - else: - args.append(arg.id) - addArgs(node.args.args) - defaults = node.args.defaults - else: - if PY38_PLUS: - for arg in node.args.posonlyargs: - args.append(arg.arg) - annotations.append(arg.annotation) - for arg in node.args.args + node.args.kwonlyargs: + if PY38_PLUS: + for arg in node.args.posonlyargs: args.append(arg.arg) annotations.append(arg.annotation) - defaults = node.args.defaults + node.args.kw_defaults + for arg in node.args.args + node.args.kwonlyargs: + args.append(arg.arg) + annotations.append(arg.annotation) + defaults = node.args.defaults + node.args.kw_defaults - # Only for Python3 FunctionDefs - is_py3_func = hasattr(node, 'returns') + has_annotations = not isinstance(node, ast.Lambda) for arg_name in ('vararg', 'kwarg'): wildcard = getattr(node.args, arg_name) if not wildcard: continue - args.append(wildcard if PY2 else wildcard.arg) - if is_py3_func: - if PY2: # Python 2.7 - argannotation = arg_name + 'annotation' - annotations.append(getattr(node.args, argannotation)) - else: # Python >= 3.4 - annotations.append(wildcard.annotation) + args.append(wildcard.arg) + if has_annotations: + annotations.append(wildcard.annotation) - if is_py3_func: + if has_annotations: annotations.append(node.returns) if len(set(args)) < len(args): @@ -2187,28 +2099,12 @@ self.report(messages.UnusedVariable, binding.source, name) self.deferAssignment(checkUnusedAssignments) - if PY2: - def checkReturnWithArgumentInsideGenerator(): - """ - Check to see if there is any return statement with - arguments but the function is a generator. - """ - if self.scope.isGenerator and self.scope.returnValue: - self.report(messages.ReturnWithArgsInsideGenerator, - self.scope.returnValue) - self.deferAssignment(checkReturnWithArgumentInsideGenerator) self.popScope() self.deferFunction(runFunction) def ARGUMENTS(self, node): self.handleChildren(node, omit=('defaults', 'kw_defaults')) - if PY2: - scope_node = self.getScopeNode(node) - if node.vararg: - self.addBinding(node, Argument(node.vararg, scope_node)) - if node.kwarg: - self.addBinding(node, Argument(node.kwarg, scope_node)) def ARG(self, node): self.addBinding(node, Argument(node.arg, self.getScopeNode(node))) @@ -2223,9 +2119,8 @@ self.handleNode(deco, node) for baseNode in node.bases: self.handleNode(baseNode, node) - if not PY2: - for keywordNode in node.keywords: - self.handleNode(keywordNode, node) + for keywordNode in node.keywords: + self.handleNode(keywordNode, node) self.pushScope(ClassScope) # doctest does not process doctest within a doctest # classes within classes are processed. @@ -2244,7 +2139,7 @@ self.handleNode(node.target, node) def TUPLE(self, node): - if not PY2 and isinstance(node.ctx, ast.Store): + if isinstance(node.ctx, ast.Store): # Python 3 advanced tuple unpacking: a, *b, c = d. # Only one starred expression is allowed, and no more than 1<<8 # assignments are allowed before a stared expression. There is @@ -2280,8 +2175,7 @@ def IMPORTFROM(self, node): if node.module == '__future__': if not self.futuresAllowed: - self.report(messages.LateFutureImport, - node, [n.name for n in node.names]) + self.report(messages.LateFutureImport, node) else: self.futuresAllowed = False @@ -2297,8 +2191,7 @@ if alias.name == 'annotations': self.annotationsFutureEnabled = True elif alias.name == '*': - # Only Python 2, local import * is a SyntaxWarning - if not PY2 and not isinstance(self.scope, ModuleScope): + if not isinstance(self.scope, ModuleScope): self.report(messages.ImportStarNotPermitted, node, module) continue @@ -2331,10 +2224,10 @@ # Process the other nodes: "except:", "else:", "finally:" self.handleChildren(node, omit='body') - TRYEXCEPT = TRY + TRYSTAR = TRY def EXCEPTHANDLER(self, node): - if PY2 or node.name is None: + if node.name is None: self.handleChildren(node) return @@ -2381,9 +2274,13 @@ def ANNASSIGN(self, node): self.handleNode(node.target, node) self.handleAnnotation(node.annotation, node) + # If the assignment has value, handle the *value* now. if node.value: - # If the assignment has value, handle the *value* now. - self.handleNode(node.value, node) + # If the annotation is `TypeAlias`, handle the *value* as an annotation. + if _is_typing(node.annotation, 'TypeAlias', self.scopeStack): + self.handleAnnotation(node.value, node) + else: + self.handleNode(node.value, node) def COMPARE(self, node): left = node.left