class Reader(object):
    # Reader:
    # - determines the data encoding and converts it to a unicode string,
    # - checks if characters are in allowed range,
    # - adds '\0' to the end.

    # Reader accepts
    #  - a `str` object (PY2) / a `bytes` object (PY3),
    #  - a `unicode` object (PY2) / a `str` object (PY3),
    #  - a file-like object with its `read` method returning `str`,
    #  - a file-like object with its `read` method returning `unicode`.

    # Yeah, it's ugly and slow.

    def __init__(self, stream, loader=None):
        # type: (Any, Any) -> None
        self.loader = loader
        if self.loader is not None and getattr(self.loader, "_reader", None) is None:
            self.loader._reader = self
        self.reset_reader()
        self.stream = stream  # type: Any  # as .read is called

    def reset_reader(self):
        # type: () -> None
        self.name = None  # type: Any
        self.stream_pointer = 0
        self.eof = True
        self.buffer = ""
        self.pointer = 0
        self.raw_buffer = None  # type: Any
        self.raw_decode = None
        self.encoding = None  # type: Optional[Text]
        self.index = 0
        self.line = 0
        self.column = 0

    @property
    def stream(self):
        # type: () -> Any
        try:
            return self._stream
        except AttributeError:
            raise YAMLStreamError("input stream needs to specified")

    @stream.setter
    def stream(self, val):
        # type: (Any) -> None
        if val is None:
            return
        self._stream = None
        if isinstance(val, text_type):
            self.name = "<unicode string>"
            self.check_printable(val)
            self.buffer = val + u"\0"  # type: ignore
        elif isinstance(val, binary_type):
            self.name = "<byte string>"
            self.raw_buffer = val
            self.determine_encoding()
        else:
            if not hasattr(val, "read"):
                raise YAMLStreamError("stream argument needs to have a read() method")
            self._stream = val
            self.name = getattr(self.stream, "name", "<file>")
            self.eof = False
            self.raw_buffer = None
            self.determine_encoding()

    def peek(self, index=0):
        # type: (int) -> Text
        try:
            return self.buffer[self.pointer + index]
        except IndexError:
            self.update(index + 1)
            return self.buffer[self.pointer + index]

    def prefix(self, length=1):
        # type: (int) -> Any
        if self.pointer + length >= len(self.buffer):
            self.update(length)
        return self.buffer[self.pointer : self.pointer + length]

    def forward_1_1(self, length=1):
        # type: (int) -> None
        if self.pointer + length + 1 >= len(self.buffer):
            self.update(length + 1)
        while length != 0:
            ch = self.buffer[self.pointer]
            self.pointer += 1
            self.index += 1
            if ch in u"\n\x85\u2028\u2029" or (
                ch == u"\r" and self.buffer[self.pointer] != u"\n"
            ):
                self.line += 1
                self.column = 0
            elif ch != u"\uFEFF":
                self.column += 1
            length -= 1

    def forward(self, length=1):
        # type: (int) -> None
        if self.pointer + length + 1 >= len(self.buffer):
            self.update(length + 1)
        while length != 0:
            ch = self.buffer[self.pointer]
            self.pointer += 1
            self.index += 1
            if ch == u"\n" or (ch == u"\r" and self.buffer[self.pointer] != u"\n"):
                self.line += 1
                self.column = 0
            elif ch != u"\uFEFF":
                self.column += 1
            length -= 1

    def get_mark(self):
        # type: () -> Any
        if self.stream is None:
            return StringMark(
                self.name, self.index, self.line, self.column, self.buffer, self.pointer
            )
        else:
            return FileMark(self.name, self.index, self.line, self.column)

    def determine_encoding(self):
        # type: () -> None
        while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2):
            self.update_raw()
        if isinstance(self.raw_buffer, binary_type):
            if self.raw_buffer.startswith(codecs.BOM_UTF16_LE):
                self.raw_decode = codecs.utf_16_le_decode  # type: ignore
                self.encoding = "utf-16-le"
            elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE):
                self.raw_decode = codecs.utf_16_be_decode  # type: ignore
                self.encoding = "utf-16-be"
            else:
                self.raw_decode = codecs.utf_8_decode  # type: ignore
                self.encoding = "utf-8"
        self.update(1)

    if UNICODE_SIZE == 2:
        NON_PRINTABLE = RegExp(
            u"[^\x09\x0A\x0D\x20-\x7E\x85" u"\xA0-\uD7FF" u"\uE000-\uFFFD" u"]"
        )
    else:
        NON_PRINTABLE = RegExp(
            u"[^\x09\x0A\x0D\x20-\x7E\x85"
            u"\xA0-\uD7FF"
            u"\uE000-\uFFFD"
            u"\U00010000-\U0010FFFF"
            u"]"
        )

    _printable_ascii = ("\x09\x0A\x0D" + "".join(map(chr, range(0x20, 0x7F)))).encode(
        "ascii"
    )

    @classmethod
    def _get_non_printable_ascii(cls, data):  # type: ignore
        # type: (Text, bytes) -> Optional[Tuple[int, Text]]
        ascii_bytes = data.encode("ascii")
        non_printables = ascii_bytes.translate(
            None, cls._printable_ascii
        )  # type: ignore
        if not non_printables:
            return None
        non_printable = non_printables[:1]
        return ascii_bytes.index(non_printable), non_printable.decode("ascii")

    @classmethod
    def _get_non_printable_regex(cls, data):
        # type: (Text) -> Optional[Tuple[int, Text]]
        match = cls.NON_PRINTABLE.search(data)
        if not bool(match):
            return None
        return match.start(), match.group()

    @classmethod
    def _get_non_printable(cls, data):
        # type: (Text) -> Optional[Tuple[int, Text]]
        try:
            return cls._get_non_printable_ascii(data)  # type: ignore
        except UnicodeEncodeError:
            return cls._get_non_printable_regex(data)

    def check_printable(self, data):
        # type: (Any) -> None
        non_printable_match = self._get_non_printable(data)
        if non_printable_match is not None:
            start, character = non_printable_match
            position = self.index + (len(self.buffer) - self.pointer) + start
            raise ReaderError(
                self.name,
                position,
                ord(character),
                "unicode",
                "special characters are not allowed",
            )

    def update(self, length):
        # type: (int) -> None
        if self.raw_buffer is None:
            return
        self.buffer = self.buffer[self.pointer :]
        self.pointer = 0
        while len(self.buffer) < length:
            if not self.eof:
                self.update_raw()
            if self.raw_decode is not None:
                try:
                    data, converted = self.raw_decode(
                        self.raw_buffer, "strict", self.eof
                    )
                except UnicodeDecodeError as exc:
                    if PY3:
                        character = self.raw_buffer[exc.start]
                    else:
                        character = exc.object[exc.start]
                    if self.stream is not None:
                        position = (
                            self.stream_pointer - len(self.raw_buffer) + exc.start
                        )
                    elif self.stream is not None:
                        position = (
                            self.stream_pointer - len(self.raw_buffer) + exc.start
                        )
                    else:
                        position = exc.start
                    raise ReaderError(
                        self.name, position, character, exc.encoding, exc.reason
                    )
            else:
                data = self.raw_buffer
                converted = len(data)
            self.check_printable(data)
            self.buffer += data
            self.raw_buffer = self.raw_buffer[converted:]
            if self.eof:
                self.buffer += "\0"
                self.raw_buffer = None
                break

    def update_raw(self, size=None):
        # type: (Optional[int]) -> None
        if size is None:
            size = 4096 if PY3 else 1024
        data = self.stream.read(size)
        if self.raw_buffer is None:
            self.raw_buffer = data
        else:
            self.raw_buffer += data
        self.stream_pointer += len(data)
        if not data:
            self.eof = True class Scanner(object):
    def __init__(self, loader=None):
        # type: (Any) -> None
        """Initialize the scanner."""
        # It is assumed that Scanner and Reader will have a common descendant.
        # Reader do the dirty work of checking for BOM and converting the
        # input data to Unicode. It also adds NUL to the end.
        #
        # Reader supports the following methods
        #   self.peek(i=0)    # peek the next i-th character
        #   self.prefix(l=1)  # peek the next l characters
        #   self.forward(l=1) # read the next l characters and move the pointer

        self.loader = loader
        if self.loader is not None and getattr(self.loader, "_scanner", None) is None:
            self.loader._scanner = self
        self.reset_scanner()
        self.first_time = False
        self.yaml_version = None  # type: Any

    @property
    def flow_level(self):
        # type: () -> int
        return len(self.flow_context)

    def reset_scanner(self):
        # type: () -> None
        # Had we reached the end of the stream?
        self.done = False

        # flow_context is an expanding/shrinking list consisting of '{' and '['
        # for each unclosed flow context. If empty list that means block context
        self.flow_context = []  # type: List[Text]

        # List of processed tokens that are not yet emitted.
        self.tokens = []  # type: List[Any]

        # Add the STREAM-START token.
        self.fetch_stream_start()

        # Number of tokens that were emitted through the `get_token` method.
        self.tokens_taken = 0

        # The current indentation level.
        self.indent = -1

        # Past indentation levels.
        self.indents = []  # type: List[int]

        # Variables related to simple keys treatment.

        # A simple key is a key that is not denoted by the '?' indicator.
        # Example of simple keys:
        #   ---
        #   block simple key: value
        #   ? not a simple key:
        #   : { flow simple key: value }
        # We emit the KEY token before all keys, so when we find a potential
        # simple key, we try to locate the corresponding ':' indicator.
        # Simple keys should be limited to a single line and 1024 characters.

        # Can a simple key start at the current position? A simple key may
        # start:
        # - at the beginning of the line, not counting indentation spaces
        #       (in block context),
        # - after '{', '[', ',' (in the flow context),
        # - after '?', ':', '-' (in the block context).
        # In the block context, this flag also signifies if a block collection
        # may start at the current position.
        self.allow_simple_key = True

        # Keep track of possible simple keys. This is a dictionary. The key
        # is `flow_level`; there can be no more that one possible simple key
        # for each level. The value is a SimpleKey record:
        #   (token_number, required, index, line, column, mark)
        # A simple key may start with ALIAS, ANCHOR, TAG, SCALAR(flow),
        # '[', or '{' tokens.
        self.possible_simple_keys = {}  # type: Dict[Any, Any]

    @property
    def reader(self):
        # type: () -> Any
        try:
            return self._scanner_reader  # type: ignore
        except AttributeError:
            if hasattr(self.loader, "typ"):
                self._scanner_reader = self.loader.reader
            else:
                self._scanner_reader = self.loader._reader
            return self._scanner_reader

    @property
    def scanner_processing_version(self):  # prefix until un-composited
        # type: () -> Any
        if hasattr(self.loader, "typ"):
            return self.loader.resolver.processing_version
        return self.loader.processing_version

    # Public methods.

    def check_token(self, *choices):
        # type: (Any) -> bool
        # Check if the next token is one of the given types.
        while self.need_more_tokens():
            self.fetch_more_tokens()
        if bool(self.tokens):
            if not choices:
                return True
            for choice in choices:
                if isinstance(self.tokens[0], choice):
                    return True
        return False

    def peek_token(self):
        # type: () -> Any
        # Return the next token, but do not delete if from the queue.
        while self.need_more_tokens():
            self.fetch_more_tokens()
        if bool(self.tokens):
            return self.tokens[0]

    def get_token(self):
        # type: () -> Any
        # Return the next token.
        while self.need_more_tokens():
            self.fetch_more_tokens()
        if bool(self.tokens):
            self.tokens_taken += 1
            return self.tokens.pop(0)

    # Private methods.

    def need_more_tokens(self):
        # type: () -> bool
        if self.done:
            return False
        if not self.tokens:
            return True
        # The current token may be a potential simple key, so we
        # need to look further.
        self.stale_possible_simple_keys()
        if self.next_possible_simple_key() == self.tokens_taken:
            return True
        return False

    def fetch_comment(self, comment):
        # type: (Any) -> None
        raise NotImplementedError

    def fetch_more_tokens(self):
        # type: () -> Any
        # Eat whitespaces and comments until we reach the next token.
        comment = self.scan_to_next_token()
        if comment is not None:  # never happens for base scanner
            return self.fetch_comment(comment)
        # Remove obsolete possible simple keys.
        self.stale_possible_simple_keys()

        # Compare the current indentation and column. It may add some tokens
        # and decrease the current indentation level.
        self.unwind_indent(self.reader.column)

        # Peek the next character.
        ch = self.reader.peek()

        # Is it the end of stream?
        if ch == "\0":
            return self.fetch_stream_end()

        # Is it a directive?
        if ch == "%" and self.check_directive():
            return self.fetch_directive()

        # Is it the document start?
        if ch == "-" and self.check_document_start():
            return self.fetch_document_start()

        # Is it the document end?
        if ch == "." and self.check_document_end():
            return self.fetch_document_end()

        # TODO: support for BOM within a stream.
        # if ch == u'\uFEFF':
        #     return self.fetch_bom()    <-- issue BOMToken

        # Note: the order of the following checks is NOT significant.

        # Is it the flow sequence start indicator?
        if ch == "[":
            return self.fetch_flow_sequence_start()

        # Is it the flow mapping start indicator?
        if ch == "{":
            return self.fetch_flow_mapping_start()

        # Is it the flow sequence end indicator?
        if ch == "]":
            return self.fetch_flow_sequence_end()

        # Is it the flow mapping end indicator?
        if ch == "}":
            return self.fetch_flow_mapping_end()

        # Is it the flow entry indicator?
        if ch == ",":
            return self.fetch_flow_entry()

        # Is it the block entry indicator?
        if ch == "-" and self.check_block_entry():
            return self.fetch_block_entry()

        # Is it the key indicator?
        if ch == "?" and self.check_key():
            return self.fetch_key()

        # Is it the value indicator?
        if ch == ":" and self.check_value():
            return self.fetch_value()

        # Is it an alias?
        if ch == "*":
            return self.fetch_alias()

        # Is it an anchor?
        if ch == "&":
            return self.fetch_anchor()

        # Is it a tag?
        if ch == "!":
            return self.fetch_tag()

        # Is it a literal scalar?
        if ch == "|" and not self.flow_level:
            return self.fetch_literal()

        # Is it a folded scalar?
        if ch == ">" and not self.flow_level:
            return self.fetch_folded()

        # Is it a single quoted scalar?
        if ch == "'":
            return self.fetch_single()

        # Is it a double quoted scalar?
        if ch == '"':
            return self.fetch_double()

        # It must be a plain scalar then.
        if self.check_plain():
            return self.fetch_plain()

        # No? It's an error. Let's produce a nice error message.
        raise ScannerError(
            "while scanning for the next token",
            None,
            "found character %r that cannot start any token" % utf8(ch),
            self.reader.get_mark(),
        )

    # Simple keys treatment.

    def next_possible_simple_key(self):
        # type: () -> Any
        # Return the number of the nearest possible simple key. Actually we
        # don't need to loop through the whole dictionary. We may replace it
        # with the following code:
        #   if not self.possible_simple_keys:
        #       return None
        #   return self.possible_simple_keys[
        #           min(self.possible_simple_keys.keys())].token_number
        min_token_number = None
        for level in self.possible_simple_keys:
            key = self.possible_simple_keys[level]
            if min_token_number is None or key.token_number < min_token_number:
                min_token_number = key.token_number
        return min_token_number

    def stale_possible_simple_keys(self):
        # type: () -> None
        # Remove entries that are no longer possible simple keys. According to
        # the YAML specification, simple keys
        # - should be limited to a single line,
        # - should be no longer than 1024 characters.
        # Disabling this procedure will allow simple keys of any length and
        # height (may cause problems if indentation is broken though).
        for level in list(self.possible_simple_keys):
            key = self.possible_simple_keys[level]
            if key.line != self.reader.line or self.reader.index - key.index > 1024:
                if key.required:
                    raise ScannerError(
                        "while scanning a simple key",
                        key.mark,
                        "could not find expected ':'",
                        self.reader.get_mark(),
                    )
                del self.possible_simple_keys[level]

    def save_possible_simple_key(self):
        # type: () -> None
        # The next token may start a simple key. We check if it's possible
        # and save its position. This function is called for
        #   ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'.

        # Check if a simple key is required at the current position.
        required = not self.flow_level and self.indent == self.reader.column

        # The next token might be a simple key. Let's save it's number and
        # position.
        if self.allow_simple_key:
            self.remove_possible_simple_key()
            token_number = self.tokens_taken + len(self.tokens)
            key = SimpleKey(
                token_number,
                required,
                self.reader.index,
                self.reader.line,
                self.reader.column,
                self.reader.get_mark(),
            )
            self.possible_simple_keys[self.flow_level] = key

    def remove_possible_simple_key(self):
        # type: () -> None
        # Remove the saved possible key position at the current flow level.
        if self.flow_level in self.possible_simple_keys:
            key = self.possible_simple_keys[self.flow_level]

            if key.required:
                raise ScannerError(
                    "while scanning a simple key",
                    key.mark,
                    "could not find expected ':'",
                    self.reader.get_mark(),
                )

            del self.possible_simple_keys[self.flow_level]

    # Indentation functions.

    def unwind_indent(self, column):
        # type: (Any) -> None
        # In flow context, tokens should respect indentation.
        # Actually the condition should be `self.indent >= column` according to
        # the spec. But this condition will prohibit intuitively correct
        # constructions such as
        # key : {
        # }
        # ####
        # if self.flow_level and self.indent > column:
        #     raise ScannerError(None, None,
        #             "invalid intendation or unclosed '[' or '{'",
        #             self.reader.get_mark())

        # In the flow context, indentation is ignored. We make the scanner less
        # restrictive then specification requires.
        if bool(self.flow_level):
            return

        # In block context, we may need to issue the BLOCK-END tokens.
        while self.indent > column:
            mark = self.reader.get_mark()
            self.indent = self.indents.pop()
            self.tokens.append(BlockEndToken(mark, mark))

    def add_indent(self, column):
        # type: (int) -> bool
        # Check if we need to increase indentation.
        if self.indent < column:
            self.indents.append(self.indent)
            self.indent = column
            return True
        return False

    # Fetchers.

    def fetch_stream_start(self):
        # type: () -> None
        # We always add STREAM-START as the first token and STREAM-END as the
        # last token.
        # Read the token.
        mark = self.reader.get_mark()
        # Add STREAM-START.
        self.tokens.append(StreamStartToken(mark, mark, encoding=self.reader.encoding))

    def fetch_stream_end(self):
        # type: () -> None
        # Set the current intendation to -1.
        self.unwind_indent(-1)
        # Reset simple keys.
        self.remove_possible_simple_key()
        self.allow_simple_key = False
        self.possible_simple_keys = {}
        # Read the token.
        mark = self.reader.get_mark()
        # Add STREAM-END.
        self.tokens.append(StreamEndToken(mark, mark))
        # The steam is finished.
        self.done = True

    def fetch_directive(self):
        # type: () -> None
        # Set the current intendation to -1.
        self.unwind_indent(-1)

        # Reset simple keys.
        self.remove_possible_simple_key()
        self.allow_simple_key = False

        # Scan and add DIRECTIVE.
        self.tokens.append(self.scan_directive())

    def fetch_document_start(self):
        # type: () -> None
        self.fetch_document_indicator(DocumentStartToken)

    def fetch_document_end(self):
        # type: () -> None
        self.fetch_document_indicator(DocumentEndToken)

    def fetch_document_indicator(self, TokenClass):
        # type: (Any) -> None
        # Set the current intendation to -1.
        self.unwind_indent(-1)

        # Reset simple keys. Note that there could not be a block collection
        # after '---'.
        self.remove_possible_simple_key()
        self.allow_simple_key = False

        # Add DOCUMENT-START or DOCUMENT-END.
        start_mark = self.reader.get_mark()
        self.reader.forward(3)
        end_mark = self.reader.get_mark()
        self.tokens.append(TokenClass(start_mark, end_mark))

    def fetch_flow_sequence_start(self):
        # type: () -> None
        self.fetch_flow_collection_start(FlowSequenceStartToken, to_push="[")

    def fetch_flow_mapping_start(self):
        # type: () -> None
        self.fetch_flow_collection_start(FlowMappingStartToken, to_push="{")

    def fetch_flow_collection_start(self, TokenClass, to_push):
        # type: (Any, Text) -> None
        # '[' and '{' may start a simple key.
        self.save_possible_simple_key()
        # Increase the flow level.
        self.flow_context.append(to_push)
        # Simple keys are allowed after '[' and '{'.
        self.allow_simple_key = True
        # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START.
        start_mark = self.reader.get_mark()
        self.reader.forward()
        end_mark = self.reader.get_mark()
        self.tokens.append(TokenClass(start_mark, end_mark))

    def fetch_flow_sequence_end(self):
        # type: () -> None
        self.fetch_flow_collection_end(FlowSequenceEndToken)

    def fetch_flow_mapping_end(self):
        # type: () -> None
        self.fetch_flow_collection_end(FlowMappingEndToken)

    def fetch_flow_collection_end(self, TokenClass):
        # type: (Any) -> None
        # Reset possible simple key on the current level.
        self.remove_possible_simple_key()
        # Decrease the flow level.
        try:
            popped = self.flow_context.pop()  # NOQA
        except IndexError:
            # We must not be in a list or object.
            # Defer error handling to the parser.
            pass
        # No simple keys after ']' or '}'.
        self.allow_simple_key = False
        # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END.
        start_mark = self.reader.get_mark()
        self.reader.forward()
        end_mark = self.reader.get_mark()
        self.tokens.append(TokenClass(start_mark, end_mark))

    def fetch_flow_entry(self):
        # type: () -> None
        # Simple keys are allowed after ','.
        self.allow_simple_key = True
        # Reset possible simple key on the current level.
        self.remove_possible_simple_key()
        # Add FLOW-ENTRY.
        start_mark = self.reader.get_mark()
        self.reader.forward()
        end_mark = self.reader.get_mark()
        self.tokens.append(FlowEntryToken(start_mark, end_mark))

    def fetch_block_entry(self):
        # type: () -> None
        # Block context needs additional checks.
        if not self.flow_level:
            # Are we allowed to start a new entry?
            if not self.allow_simple_key:
                raise ScannerError(
                    None,
                    None,
                    "sequence entries are not allowed here",
                    self.reader.get_mark(),
                )
            # We may need to add BLOCK-SEQUENCE-START.
            if self.add_indent(self.reader.column):
                mark = self.reader.get_mark()
                self.tokens.append(BlockSequenceStartToken(mark, mark))
        # It's an error for the block entry to occur in the flow context,
        # but we let the parser detect this.
        else:
            pass
        # Simple keys are allowed after '-'.
        self.allow_simple_key = True
        # Reset possible simple key on the current level.
        self.remove_possible_simple_key()

        # Add BLOCK-ENTRY.
        start_mark = self.reader.get_mark()
        self.reader.forward()
        end_mark = self.reader.get_mark()
        self.tokens.append(BlockEntryToken(start_mark, end_mark))

    def fetch_key(self):
        # type: () -> None
        # Block context needs additional checks.
        if not self.flow_level:

            # Are we allowed to start a key (not nessesary a simple)?
            if not self.allow_simple_key:
                raise ScannerError(
                    None,
                    None,
                    "mapping keys are not allowed here",
                    self.reader.get_mark(),
                )

            # We may need to add BLOCK-MAPPING-START.
            if self.add_indent(self.reader.column):
                mark = self.reader.get_mark()
                self.tokens.append(BlockMappingStartToken(mark, mark))

        # Simple keys are allowed after '?' in the block context.
        self.allow_simple_key = not self.flow_level

        # Reset possible simple key on the current level.
        self.remove_possible_simple_key()

        # Add KEY.
        start_mark = self.reader.get_mark()
        self.reader.forward()
        end_mark = self.reader.get_mark()
        self.tokens.append(KeyToken(start_mark, end_mark))

    def fetch_value(self):
        # type: () -> None
        # Do we determine a simple key?
        if self.flow_level in self.possible_simple_keys:
            # Add KEY.
            key = self.possible_simple_keys[self.flow_level]
            del self.possible_simple_keys[self.flow_level]
            self.tokens.insert(
                key.token_number - self.tokens_taken, KeyToken(key.mark, key.mark)
            )

            # If this key starts a new block mapping, we need to add
            # BLOCK-MAPPING-START.
            if not self.flow_level:
                if self.add_indent(key.column):
                    self.tokens.insert(
                        key.token_number - self.tokens_taken,
                        BlockMappingStartToken(key.mark, key.mark),
                    )

            # There cannot be two simple keys one after another.
            self.allow_simple_key = False

        # It must be a part of a complex key.
        else:

            # Block context needs additional checks.
            # (Do we really need them? They will be caught by the parser
            # anyway.)
            if not self.flow_level:

                # We are allowed to start a complex value if and only if
                # we can start a simple key.
                if not self.allow_simple_key:
                    raise ScannerError(
                        None,
                        None,
                        "mapping values are not allowed here",
                        self.reader.get_mark(),
                    )

            # If this value starts a new block mapping, we need to add
            # BLOCK-MAPPING-START.  It will be detected as an error later by
            # the parser.
            if not self.flow_level:
                if self.add_indent(self.reader.column):
                    mark = self.reader.get_mark()
                    self.tokens.append(BlockMappingStartToken(mark, mark))

            # Simple keys are allowed after ':' in the block context.
            self.allow_simple_key = not self.flow_level

            # Reset possible simple key on the current level.
            self.remove_possible_simple_key()

        # Add VALUE.
        start_mark = self.reader.get_mark()
        self.reader.forward()
        end_mark = self.reader.get_mark()
        self.tokens.append(ValueToken(start_mark, end_mark))

    def fetch_alias(self):
        # type: () -> None
        # ALIAS could be a simple key.
        self.save_possible_simple_key()
        # No simple keys after ALIAS.
        self.allow_simple_key = False
        # Scan and add ALIAS.
        self.tokens.append(self.scan_anchor(AliasToken))

    def fetch_anchor(self):
        # type: () -> None
        # ANCHOR could start a simple key.
        self.save_possible_simple_key()
        # No simple keys after ANCHOR.
        self.allow_simple_key = False
        # Scan and add ANCHOR.
        self.tokens.append(self.scan_anchor(AnchorToken))

    def fetch_tag(self):
        # type: () -> None
        # TAG could start a simple key.
        self.save_possible_simple_key()
        # No simple keys after TAG.
        self.allow_simple_key = False
        # Scan and add TAG.
        self.tokens.append(self.scan_tag())

    def fetch_literal(self):
        # type: () -> None
        self.fetch_block_scalar(style="|")

    def fetch_folded(self):
        # type: () -> None
        self.fetch_block_scalar(style=">")

    def fetch_block_scalar(self, style):
        # type: (Any) -> None
        # A simple key may follow a block scalar.
        self.allow_simple_key = True
        # Reset possible simple key on the current level.
        self.remove_possible_simple_key()
        # Scan and add SCALAR.
        self.tokens.append(self.scan_block_scalar(style))

    def fetch_single(self):
        # type: () -> None
        self.fetch_flow_scalar(style="'")

    def fetch_double(self):
        # type: () -> None
        self.fetch_flow_scalar(style='"')

    def fetch_flow_scalar(self, style):
        # type: (Any) -> None
        # A flow scalar could be a simple key.
        self.save_possible_simple_key()
        # No simple keys after flow scalars.
        self.allow_simple_key = False
        # Scan and add SCALAR.
        self.tokens.append(self.scan_flow_scalar(style))

    def fetch_plain(self):
        # type: () -> None
        # A plain scalar could be a simple key.
        self.save_possible_simple_key()
        # No simple keys after plain scalars. But note that `scan_plain` will
        # change this flag if the scan is finished at the beginning of the
        # line.
        self.allow_simple_key = False
        # Scan and add SCALAR. May change `allow_simple_key`.
        self.tokens.append(self.scan_plain())

    # Checkers.

    def check_directive(self):
        # type: () -> Any
        # DIRECTIVE:        ^ '%' ...
        # The '%' indicator is already checked.
        if self.reader.column == 0:
            return True
        return None

    def check_document_start(self):
        # type: () -> Any
        # DOCUMENT-START:   ^ '---' (' '|'\n')
        if self.reader.column == 0:
            if (
                self.reader.prefix(3) == "---"
                and self.reader.peek(3) in _THE_END_SPACE_TAB
            ):
                return True
        return None

    def check_document_end(self):
        # type: () -> Any
        # DOCUMENT-END:     ^ '...' (' '|'\n')
        if self.reader.column == 0:
            if (
                self.reader.prefix(3) == "..."
                and self.reader.peek(3) in _THE_END_SPACE_TAB
            ):
                return True
        return None

    def check_block_entry(self):
        # type: () -> Any
        # BLOCK-ENTRY:      '-' (' '|'\n')
        return self.reader.peek(1) in _THE_END_SPACE_TAB

    def check_key(self):
        # type: () -> Any
        # KEY(flow context):    '?'
        if bool(self.flow_level):
            return True
        # KEY(block context):   '?' (' '|'\n')
        return self.reader.peek(1) in _THE_END_SPACE_TAB

    def check_value(self):
        # type: () -> Any
        # VALUE(flow context):  ':'
        if self.scanner_processing_version == (1, 1):
            if bool(self.flow_level):
                return True
        else:
            if bool(self.flow_level):
                if self.flow_context[-1] == "[":
                    if self.reader.peek(1) not in _THE_END_SPACE_TAB:
                        return False
                elif self.tokens and isinstance(self.tokens[-1], ValueToken):
                    # mapping flow context scanning a value token
                    if self.reader.peek(1) not in _THE_END_SPACE_TAB:
                        return False
                return True
        # VALUE(block context): ':' (' '|'\n')
        return self.reader.peek(1) in _THE_END_SPACE_TAB

    def check_plain(self):
        # type: () -> Any
        # A plain scalar may start with any non-space character except:
        #   '-', '?', ':', ',', '[', ']', '{', '}',
        #   '#', '&', '*', '!', '|', '>', '\'', '\"',
        #   '%', '@', '`'.
        #
        # It may also start with
        #   '-', '?', ':'
        # if it is followed by a non-space character.
        #
        # Note that we limit the last rule to the block context (except the
        # '-' character) because we want the flow context to be space
        # independent.
        srp = self.reader.peek
        ch = srp()
        if self.scanner_processing_version == (1, 1):
            return ch not in "\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>'\"%@`" or (
                srp(1) not in _THE_END_SPACE_TAB
                and (ch == "-" or (not self.flow_level and ch in "?:"))
            )
        # YAML 1.2
        if ch not in "\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>'\"%@`":
            # ###################                ^ ???
            return True
        ch1 = srp(1)
        if ch == "-" and ch1 not in _THE_END_SPACE_TAB:
            return True
        if ch == ":" and bool(self.flow_level) and ch1 not in _SPACE_TAB:
            return True

        return srp(1) not in _THE_END_SPACE_TAB and (
            ch == "-" or (not self.flow_level and ch in "?:")
        )

    # Scanners.

    def scan_to_next_token(self):
        # type: () -> Any
        # We ignore spaces, line breaks and comments.
        # If we find a line break in the block context, we set the flag
        # `allow_simple_key` on.
        # The byte order mark is stripped if it's the first character in the
        # stream. We do not yet support BOM inside the stream as the
        # specification requires. Any such mark will be considered as a part
        # of the document.
        #
        # TODO: We need to make tab handling rules more sane. A good rule is
        #   Tabs cannot precede tokens
        #   BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END,
        #   KEY(block), VALUE(block), BLOCK-ENTRY
        # So the checking code is
        #   if <TAB>:
        #       self.allow_simple_keys = False
        # We also need to add the check for `allow_simple_keys == True` to
        # `unwind_indent` before issuing BLOCK-END.
        # Scanners for block, flow, and plain scalars need to be modified.
        srp = self.reader.peek
        srf = self.reader.forward
        if self.reader.index == 0 and srp() == "\uFEFF":
            srf()
        found = False
        _the_end = _THE_END
        while not found:
            while srp() == " ":
                srf()
            if srp() == "#":
                while srp() not in _the_end:
                    srf()
            if self.scan_line_break():
                if not self.flow_level:
                    self.allow_simple_key = True
            else:
                found = True
        return None

    def scan_directive(self):
        # type: () -> Any
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        start_mark = self.reader.get_mark()
        srf()
        name = self.scan_directive_name(start_mark)
        value = None
        if name == "YAML":
            value = self.scan_yaml_directive_value(start_mark)
            end_mark = self.reader.get_mark()
        elif name == "TAG":
            value = self.scan_tag_directive_value(start_mark)
            end_mark = self.reader.get_mark()
        else:
            end_mark = self.reader.get_mark()
            while srp() not in _THE_END:
                srf()
        self.scan_directive_ignored_line(start_mark)
        return DirectiveToken(name, value, start_mark, end_mark)

    def scan_directive_name(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        length = 0
        srp = self.reader.peek
        ch = srp(length)
        while "0" <= ch <= "9" or "A" <= ch <= "Z" or "a" <= ch <= "z" or ch in "-_:.":
            length += 1
            ch = srp(length)
        if not length:
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected alphabetic or numeric character, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        value = self.reader.prefix(length)
        self.reader.forward(length)
        ch = srp()
        if ch not in "\0 \r\n\x85\u2028\u2029":
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected alphabetic or numeric character, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        return value

    def scan_yaml_directive_value(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        while srp() == " ":
            srf()
        major = self.scan_yaml_directive_number(start_mark)
        if srp() != ".":
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected a digit or '.', but found %r" % utf8(srp()),
                self.reader.get_mark(),
            )
        srf()
        minor = self.scan_yaml_directive_number(start_mark)
        if srp() not in "\0 \r\n\x85\u2028\u2029":
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected a digit or ' ', but found %r" % utf8(srp()),
                self.reader.get_mark(),
            )
        self.yaml_version = (major, minor)
        return self.yaml_version

    def scan_yaml_directive_number(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        ch = srp()
        if not ("0" <= ch <= "9"):
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected a digit, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        length = 0
        while "0" <= srp(length) <= "9":
            length += 1
        value = int(self.reader.prefix(length))
        srf(length)
        return value

    def scan_tag_directive_value(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        while srp() == " ":
            srf()
        handle = self.scan_tag_directive_handle(start_mark)
        while srp() == " ":
            srf()
        prefix = self.scan_tag_directive_prefix(start_mark)
        return (handle, prefix)

    def scan_tag_directive_handle(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        value = self.scan_tag_handle("directive", start_mark)
        ch = self.reader.peek()
        if ch != " ":
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected ' ', but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        return value

    def scan_tag_directive_prefix(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        value = self.scan_tag_uri("directive", start_mark)
        ch = self.reader.peek()
        if ch not in "\0 \r\n\x85\u2028\u2029":
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected ' ', but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        return value

    def scan_directive_ignored_line(self, start_mark):
        # type: (Any) -> None
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        while srp() == " ":
            srf()
        if srp() == "#":
            while srp() not in _THE_END:
                srf()
        ch = srp()
        if ch not in _THE_END:
            raise ScannerError(
                "while scanning a directive",
                start_mark,
                "expected a comment or a line break, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        self.scan_line_break()

    def scan_anchor(self, TokenClass):
        # type: (Any) -> Any
        # The specification does not restrict characters for anchors and
        # aliases. This may lead to problems, for instance, the document:
        #   [ *alias, value ]
        # can be interpteted in two ways, as
        #   [ "value" ]
        # and
        #   [ *alias , "value" ]
        # Therefore we restrict aliases to numbers and ASCII letters.
        srp = self.reader.peek
        start_mark = self.reader.get_mark()
        indicator = srp()
        if indicator == "*":
            name = "alias"
        else:
            name = "anchor"
        self.reader.forward()
        length = 0
        ch = srp(length)
        # while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \
        #         or ch in u'-_':
        while check_anchorname_char(ch):
            length += 1
            ch = srp(length)
        if not length:
            raise ScannerError(
                "while scanning an %s" % (name,),
                start_mark,
                "expected alphabetic or numeric character, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        value = self.reader.prefix(length)
        self.reader.forward(length)
        # ch1 = ch
        # ch = srp()   # no need to peek, ch is already set
        # assert ch1 == ch
        if ch not in "\0 \t\r\n\x85\u2028\u2029?:,[]{}%@`":
            raise ScannerError(
                "while scanning an %s" % (name,),
                start_mark,
                "expected alphabetic or numeric character, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        end_mark = self.reader.get_mark()
        return TokenClass(value, start_mark, end_mark)

    def scan_tag(self):
        # type: () -> Any
        # See the specification for details.
        srp = self.reader.peek
        start_mark = self.reader.get_mark()
        ch = srp(1)
        if ch == "<":
            handle = None
            self.reader.forward(2)
            suffix = self.scan_tag_uri("tag", start_mark)
            if srp() != ">":
                raise ScannerError(
                    "while parsing a tag",
                    start_mark,
                    "expected '>', but found %r" % utf8(srp()),
                    self.reader.get_mark(),
                )
            self.reader.forward()
        elif ch in _THE_END_SPACE_TAB:
            handle = None
            suffix = "!"
            self.reader.forward()
        else:
            length = 1
            use_handle = False
            while ch not in "\0 \r\n\x85\u2028\u2029":
                if ch == "!":
                    use_handle = True
                    break
                length += 1
                ch = srp(length)
            handle = "!"
            if use_handle:
                handle = self.scan_tag_handle("tag", start_mark)
            else:
                handle = "!"
                self.reader.forward()
            suffix = self.scan_tag_uri("tag", start_mark)
        ch = srp()
        if ch not in "\0 \r\n\x85\u2028\u2029":
            raise ScannerError(
                "while scanning a tag",
                start_mark,
                "expected ' ', but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        value = (handle, suffix)
        end_mark = self.reader.get_mark()
        return TagToken(value, start_mark, end_mark)

    def scan_block_scalar(self, style, rt=False):
        # type: (Any, Optional[bool]) -> Any
        # See the specification for details.
        srp = self.reader.peek
        if style == ">":
            folded = True
        else:
            folded = False

        chunks = []  # type: List[Any]
        start_mark = self.reader.get_mark()

        # Scan the header.
        self.reader.forward()
        chomping, increment = self.scan_block_scalar_indicators(start_mark)
        # block scalar comment e.g. : |+  # comment text
        block_scalar_comment = self.scan_block_scalar_ignored_line(start_mark)

        # Determine the indentation level and go to the first non-empty line.
        min_indent = self.indent + 1
        if increment is None:
            # no increment and top level, min_indent could be 0
            if min_indent < 1 and (
                style not in "|>"
                or (self.scanner_processing_version == (1, 1))
                and getattr(
                    self.loader,
                    "top_level_block_style_scalar_no_indent_error_1_1",
                    False,
                )
            ):
                min_indent = 1
            breaks, max_indent, end_mark = self.scan_block_scalar_indentation()
            indent = max(min_indent, max_indent)
        else:
            if min_indent < 1:
                min_indent = 1
            indent = min_indent + increment - 1
            breaks, end_mark = self.scan_block_scalar_breaks(indent)
        line_break = ""

        # Scan the inner part of the block scalar.
        while self.reader.column == indent and srp() != "\0":
            chunks.extend(breaks)
            leading_non_space = srp() not in " \t"
            length = 0
            while srp(length) not in _THE_END:
                length += 1
            chunks.append(self.reader.prefix(length))
            self.reader.forward(length)
            line_break = self.scan_line_break()
            breaks, end_mark = self.scan_block_scalar_breaks(indent)
            if style in "|>" and min_indent == 0:
                # at the beginning of a line, if in block style see if
                # end of document/start_new_document
                if self.check_document_start() or self.check_document_end():
                    break
            if self.reader.column == indent and srp() != "\0":

                # Unfortunately, folding rules are ambiguous.
                #
                # This is the folding according to the specification:

                if rt and folded and line_break == "\n":
                    chunks.append("\a")
                if (
                    folded
                    and line_break == "\n"
                    and leading_non_space
                    and srp() not in " \t"
                ):
                    if not breaks:
                        chunks.append(" ")
                else:
                    chunks.append(line_break)

                # This is Clark Evans's interpretation (also in the spec
                # examples):
                #
                # if folded and line_break == u'\n':
                #     if not breaks:
                #         if srp() not in ' \t':
                #             chunks.append(u' ')
                #         else:
                #             chunks.append(line_break)
                # else:
                #     chunks.append(line_break)
            else:
                break

        # Process trailing line breaks. The 'chomping' setting determines
        # whether they are included in the value.
        trailing = []  # type: List[Any]
        if chomping in [None, True]:
            chunks.append(line_break)
        if chomping is True:
            chunks.extend(breaks)
        elif chomping in [None, False]:
            trailing.extend(breaks)

        # We are done.
        token = ScalarToken("".join(chunks), False, start_mark, end_mark, style)
        if block_scalar_comment is not None:
            token.add_pre_comments([block_scalar_comment])
        if len(trailing) > 0:
            # nprint('trailing 1', trailing)  # XXXXX
            # Eat whitespaces and comments until we reach the next token.
            comment = self.scan_to_next_token()
            while comment:
                trailing.append(" " * comment[1].column + comment[0])
                comment = self.scan_to_next_token()

            # Keep track of the trailing whitespace and following comments
            # as a comment token, if isn't all included in the actual value.
            comment_end_mark = self.reader.get_mark()
            comment = CommentToken("".join(trailing), end_mark, comment_end_mark)
            token.add_post_comment(comment)
        return token

    def scan_block_scalar_indicators(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        srp = self.reader.peek
        chomping = None
        increment = None
        ch = srp()
        if ch in "+-":
            if ch == "+":
                chomping = True
            else:
                chomping = False
            self.reader.forward()
            ch = srp()
            if ch in "0123456789":
                increment = int(ch)
                if increment == 0:
                    raise ScannerError(
                        "while scanning a block scalar",
                        start_mark,
                        "expected indentation indicator in the range 1-9, "
                        "but found 0",
                        self.reader.get_mark(),
                    )
                self.reader.forward()
        elif ch in "0123456789":
            increment = int(ch)
            if increment == 0:
                raise ScannerError(
                    "while scanning a block scalar",
                    start_mark,
                    "expected indentation indicator in the range 1-9, " "but found 0",
                    self.reader.get_mark(),
                )
            self.reader.forward()
            ch = srp()
            if ch in "+-":
                if ch == "+":
                    chomping = True
                else:
                    chomping = False
                self.reader.forward()
        ch = srp()
        if ch not in "\0 \r\n\x85\u2028\u2029":
            raise ScannerError(
                "while scanning a block scalar",
                start_mark,
                "expected chomping or indentation indicators, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        return chomping, increment

    def scan_block_scalar_ignored_line(self, start_mark):
        # type: (Any) -> Any
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        prefix = ""
        comment = None
        while srp() == " ":
            prefix += srp()
            srf()
        if srp() == "#":
            comment = prefix
            while srp() not in _THE_END:
                comment += srp()
                srf()
        ch = srp()
        if ch not in _THE_END:
            raise ScannerError(
                "while scanning a block scalar",
                start_mark,
                "expected a comment or a line break, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        self.scan_line_break()
        return comment

    def scan_block_scalar_indentation(self):
        # type: () -> Any
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        chunks = []
        max_indent = 0
        end_mark = self.reader.get_mark()
        while srp() in " \r\n\x85\u2028\u2029":
            if srp() != " ":
                chunks.append(self.scan_line_break())
                end_mark = self.reader.get_mark()
            else:
                srf()
                if self.reader.column > max_indent:
                    max_indent = self.reader.column
        return chunks, max_indent, end_mark

    def scan_block_scalar_breaks(self, indent):
        # type: (int) -> Any
        # See the specification for details.
        chunks = []
        srp = self.reader.peek
        srf = self.reader.forward
        end_mark = self.reader.get_mark()
        while self.reader.column < indent and srp() == " ":
            srf()
        while srp() in "\r\n\x85\u2028\u2029":
            chunks.append(self.scan_line_break())
            end_mark = self.reader.get_mark()
            while self.reader.column < indent and srp() == " ":
                srf()
        return chunks, end_mark

    def scan_flow_scalar(self, style):
        # type: (Any) -> Any
        # See the specification for details.
        # Note that we loose indentation rules for quoted scalars. Quoted
        # scalars don't need to adhere indentation because " and ' clearly
        # mark the beginning and the end of them. Therefore we are less
        # restrictive then the specification requires. We only need to check
        # that document separators are not included in scalars.
        if style == '"':
            double = True
        else:
            double = False
        srp = self.reader.peek
        chunks = []  # type: List[Any]
        start_mark = self.reader.get_mark()
        quote = srp()
        self.reader.forward()
        chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark))
        while srp() != quote:
            chunks.extend(self.scan_flow_scalar_spaces(double, start_mark))
            chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark))
        self.reader.forward()
        end_mark = self.reader.get_mark()
        return ScalarToken("".join(chunks), False, start_mark, end_mark, style)

    ESCAPE_REPLACEMENTS = {
        "0": "\0",
        "a": "\x07",
        "b": "\x08",
        "t": "\x09",
        "\t": "\x09",
        "n": "\x0A",
        "v": "\x0B",
        "f": "\x0C",
        "r": "\x0D",
        "e": "\x1B",
        " ": "\x20",
        '"': '"',
        "/": "/",  # as per http://www.json.org/
        "\\": "\\",
        "N": "\x85",
        "_": "\xA0",
        "L": "\u2028",
        "P": "\u2029",
    }

    ESCAPE_CODES = {"x": 2, "u": 4, "U": 8}

    def scan_flow_scalar_non_spaces(self, double, start_mark):
        # type: (Any, Any) -> Any
        # See the specification for details.
        chunks = []  # type: List[Any]
        srp = self.reader.peek
        srf = self.reader.forward
        while True:
            length = 0
            while srp(length) not in " \n'\"\\\0\t\r\x85\u2028\u2029":
                length += 1
            if length != 0:
                chunks.append(self.reader.prefix(length))
                srf(length)
            ch = srp()
            if not double and ch == "'" and srp(1) == "'":
                chunks.append("'")
                srf(2)
            elif (double and ch == "'") or (not double and ch in '"\\'):
                chunks.append(ch)
                srf()
            elif double and ch == "\\":
                srf()
                ch = srp()
                if ch in self.ESCAPE_REPLACEMENTS:
                    chunks.append(self.ESCAPE_REPLACEMENTS[ch])
                    srf()
                elif ch in self.ESCAPE_CODES:
                    length = self.ESCAPE_CODES[ch]
                    srf()
                    for k in range(length):
                        if srp(k) not in "0123456789ABCDEFabcdef":
                            raise ScannerError(
                                "while scanning a double-quoted scalar",
                                start_mark,
                                "expected escape sequence of %d hexdecimal "
                                "numbers, but found %r" % (length, utf8(srp(k))),
                                self.reader.get_mark(),
                            )
                    code = int(self.reader.prefix(length), 16)
                    chunks.append(unichr(code))
                    srf(length)
                elif ch in "\n\r\x85\u2028\u2029":
                    self.scan_line_break()
                    chunks.extend(self.scan_flow_scalar_breaks(double, start_mark))
                else:
                    raise ScannerError(
                        "while scanning a double-quoted scalar",
                        start_mark,
                        "found unknown escape character %r" % utf8(ch),
                        self.reader.get_mark(),
                    )
            else:
                return chunks

    def scan_flow_scalar_spaces(self, double, start_mark):
        # type: (Any, Any) -> Any
        # See the specification for details.
        srp = self.reader.peek
        chunks = []
        length = 0
        while srp(length) in " \t":
            length += 1
        whitespaces = self.reader.prefix(length)
        self.reader.forward(length)
        ch = srp()
        if ch == "\0":
            raise ScannerError(
                "while scanning a quoted scalar",
                start_mark,
                "found unexpected end of stream",
                self.reader.get_mark(),
            )
        elif ch in "\r\n\x85\u2028\u2029":
            line_break = self.scan_line_break()
            breaks = self.scan_flow_scalar_breaks(double, start_mark)
            if line_break != "\n":
                chunks.append(line_break)
            elif not breaks:
                chunks.append(" ")
            chunks.extend(breaks)
        else:
            chunks.append(whitespaces)
        return chunks

    def scan_flow_scalar_breaks(self, double, start_mark):
        # type: (Any, Any) -> Any
        # See the specification for details.
        chunks = []  # type: List[Any]
        srp = self.reader.peek
        srf = self.reader.forward
        while True:
            # Instead of checking indentation, we check for document
            # separators.
            prefix = self.reader.prefix(3)
            if (prefix == "---" or prefix == "...") and srp(3) in _THE_END_SPACE_TAB:
                raise ScannerError(
                    "while scanning a quoted scalar",
                    start_mark,
                    "found unexpected document separator",
                    self.reader.get_mark(),
                )
            while srp() in " \t":
                srf()
            if srp() in "\r\n\x85\u2028\u2029":
                chunks.append(self.scan_line_break())
            else:
                return chunks

    def scan_plain(self):
        # type: () -> Any
        # See the specification for details.
        # We add an additional restriction for the flow context:
        #   plain scalars in the flow context cannot contain ',', ': '  and '?'.
        # We also keep track of the `allow_simple_key` flag here.
        # Indentation rules are loosed for the flow context.
        srp = self.reader.peek
        srf = self.reader.forward
        chunks = []  # type: List[Any]
        start_mark = self.reader.get_mark()
        end_mark = start_mark
        indent = self.indent + 1
        # We allow zero indentation for scalars, but then we need to check for
        # document separators at the beginning of the line.
        # if indent == 0:
        #     indent = 1
        spaces = []  # type: List[Any]
        while True:
            length = 0
            if srp() == "#":
                break
            while True:
                ch = srp(length)
                if ch == ":" and srp(length + 1) not in _THE_END_SPACE_TAB:
                    pass
                elif ch == "?" and self.scanner_processing_version != (1, 1):
                    pass
                elif (
                    ch in _THE_END_SPACE_TAB
                    or (
                        not self.flow_level
                        and ch == ":"
                        and srp(length + 1) in _THE_END_SPACE_TAB
                    )
                    or (self.flow_level and ch in ",:?[]{}")
                ):
                    break
                length += 1
            # It's not clear what we should do with ':' in the flow context.
            if (
                self.flow_level
                and ch == ":"
                and srp(length + 1) not in "\0 \t\r\n\x85\u2028\u2029,[]{}"
            ):
                srf(length)
                raise ScannerError(
                    "while scanning a plain scalar",
                    start_mark,
                    "found unexpected ':'",
                    self.reader.get_mark(),
                    "Please check "
                    "http://pyyaml.org/wiki/YAMLColonInFlowContext "
                    "for details.",
                )
            if length == 0:
                break
            self.allow_simple_key = False
            chunks.extend(spaces)
            chunks.append(self.reader.prefix(length))
            srf(length)
            end_mark = self.reader.get_mark()
            spaces = self.scan_plain_spaces(indent, start_mark)
            if (
                not spaces
                or srp() == "#"
                or (not self.flow_level and self.reader.column < indent)
            ):
                break

        token = ScalarToken("".join(chunks), True, start_mark, end_mark)
        if spaces and spaces[0] == "\n":
            # Create a comment token to preserve the trailing line breaks.
            comment = CommentToken("".join(spaces) + "\n", start_mark, end_mark)
            token.add_post_comment(comment)
        return token

    def scan_plain_spaces(self, indent, start_mark):
        # type: (Any, Any) -> Any
        # See the specification for details.
        # The specification is really confusing about tabs in plain scalars.
        # We just forbid them completely. Do not use tabs in YAML!
        srp = self.reader.peek
        srf = self.reader.forward
        chunks = []
        length = 0
        while srp(length) in " ":
            length += 1
        whitespaces = self.reader.prefix(length)
        self.reader.forward(length)
        ch = srp()
        if ch in "\r\n\x85\u2028\u2029":
            line_break = self.scan_line_break()
            self.allow_simple_key = True
            prefix = self.reader.prefix(3)
            if (prefix == "---" or prefix == "...") and srp(3) in _THE_END_SPACE_TAB:
                return
            breaks = []
            while srp() in " \r\n\x85\u2028\u2029":
                if srp() == " ":
                    srf()
                else:
                    breaks.append(self.scan_line_break())
                    prefix = self.reader.prefix(3)
                    if (prefix == "---" or prefix == "...") and srp(
                        3
                    ) in _THE_END_SPACE_TAB:
                        return
            if line_break != "\n":
                chunks.append(line_break)
            elif not breaks:
                chunks.append(" ")
            chunks.extend(breaks)
        elif whitespaces:
            chunks.append(whitespaces)
        return chunks

    def scan_tag_handle(self, name, start_mark):
        # type: (Any, Any) -> Any
        # See the specification for details.
        # For some strange reasons, the specification does not allow '_' in
        # tag handles. I have allowed it anyway.
        srp = self.reader.peek
        ch = srp()
        if ch != "!":
            raise ScannerError(
                "while scanning a %s" % (name,),
                start_mark,
                "expected '!', but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        length = 1
        ch = srp(length)
        if ch != " ":
            while (
                "0" <= ch <= "9" or "A" <= ch <= "Z" or "a" <= ch <= "z" or ch in "-_"
            ):
                length += 1
                ch = srp(length)
            if ch != "!":
                self.reader.forward(length)
                raise ScannerError(
                    "while scanning a %s" % (name,),
                    start_mark,
                    "expected '!', but found %r" % utf8(ch),
                    self.reader.get_mark(),
                )
            length += 1
        value = self.reader.prefix(length)
        self.reader.forward(length)
        return value

    def scan_tag_uri(self, name, start_mark):
        # type: (Any, Any) -> Any
        # See the specification for details.
        # Note: we do not check if URI is well-formed.
        srp = self.reader.peek
        chunks = []
        length = 0
        ch = srp(length)
        while (
            "0" <= ch <= "9"
            or "A" <= ch <= "Z"
            or "a" <= ch <= "z"
            or ch in "-;/?:@&=+$,_.!~*'()[]%"
            or ((self.scanner_processing_version > (1, 1)) and ch == "#")
        ):
            if ch == "%":
                chunks.append(self.reader.prefix(length))
                self.reader.forward(length)
                length = 0
                chunks.append(self.scan_uri_escapes(name, start_mark))
            else:
                length += 1
            ch = srp(length)
        if length != 0:
            chunks.append(self.reader.prefix(length))
            self.reader.forward(length)
            length = 0
        if not chunks:
            raise ScannerError(
                "while parsing a %s" % (name,),
                start_mark,
                "expected URI, but found %r" % utf8(ch),
                self.reader.get_mark(),
            )
        return "".join(chunks)

    def scan_uri_escapes(self, name, start_mark):
        # type: (Any, Any) -> Any
        # See the specification for details.
        srp = self.reader.peek
        srf = self.reader.forward
        code_bytes = []  # type: List[Any]
        mark = self.reader.get_mark()
        while srp() == "%":
            srf()
            for k in range(2):
                if srp(k) not in "0123456789ABCDEFabcdef":
                    raise ScannerError(
                        "while scanning a %s" % (name,),
                        start_mark,
                        "expected URI escape sequence of 2 hexdecimal numbers,"
                        " but found %r" % utf8(srp(k)),
                        self.reader.get_mark(),
                    )
            if PY3:
                code_bytes.append(int(self.reader.prefix(2), 16))
            else:
                code_bytes.append(chr(int(self.reader.prefix(2), 16)))
            srf(2)
        try:
            if PY3:
                value = bytes(code_bytes).decode("utf-8")
            else:
                value = unicode(b"".join(code_bytes), "utf-8")
        except UnicodeDecodeError as exc:
            raise ScannerError(
                "while scanning a %s" % (name,), start_mark, str(exc), mark
            )
        return value

    def scan_line_break(self):
        # type: () -> Any
        # Transforms:
        #   '\r\n'      :   '\n'
        #   '\r'        :   '\n'
        #   '\n'        :   '\n'
        #   '\x85'      :   '\n'
        #   '\u2028'    :   '\u2028'
        #   '\u2029     :   '\u2029'
        #   default     :   ''
        ch = self.reader.peek()
        if ch in "\r\n\x85":
            if self.reader.prefix(2) == "\r\n":
                self.reader.forward(2)
            else:
                self.reader.forward()
            return "\n"
        elif ch in "\u2028\u2029":
            self.reader.forward()
            return ch
        return "" class RoundTripScanner(Scanner):
    def check_token(self, *choices):
        # type: (Any) -> bool
        # Check if the next token is one of the given types.
        while self.need_more_tokens():
            self.fetch_more_tokens()
        self._gather_comments()
        if bool(self.tokens):
            if not choices:
                return True
            for choice in choices:
                if isinstance(self.tokens[0], choice):
                    return True
        return False

    def peek_token(self):
        # type: () -> Any
        # Return the next token, but do not delete if from the queue.
        while self.need_more_tokens():
            self.fetch_more_tokens()
        self._gather_comments()
        if bool(self.tokens):
            return self.tokens[0]
        return None

    def _gather_comments(self):
        # type: () -> Any
        """combine multiple comment lines"""
        comments = []  # type: List[Any]
        if not self.tokens:
            return comments
        if isinstance(self.tokens[0], CommentToken):
            comment = self.tokens.pop(0)
            self.tokens_taken += 1
            comments.append(comment)
        while self.need_more_tokens():
            self.fetch_more_tokens()
            if not self.tokens:
                return comments
            if isinstance(self.tokens[0], CommentToken):
                self.tokens_taken += 1
                comment = self.tokens.pop(0)
                # nprint('dropping2', comment)
                comments.append(comment)
        if len(comments) >= 1:
            self.tokens[0].add_pre_comments(comments)
        # pull in post comment on e.g. ':'
        if not self.done and len(self.tokens) < 2:
            self.fetch_more_tokens()

    def get_token(self):
        # type: () -> Any
        # Return the next token.
        while self.need_more_tokens():
            self.fetch_more_tokens()
        self._gather_comments()
        if bool(self.tokens):
            # nprint('tk', self.tokens)
            # only add post comment to single line tokens:
            # scalar, value token. FlowXEndToken, otherwise
            # hidden streamtokens could get them (leave them and they will be
            # pre comments for the next map/seq
            if (
                len(self.tokens) > 1
                and isinstance(
                    self.tokens[0],
                    (
                        ScalarToken,
                        ValueToken,
                        FlowSequenceEndToken,
                        FlowMappingEndToken,
                    ),
                )
                and isinstance(self.tokens[1], CommentToken)
                and self.tokens[0].end_mark.line == self.tokens[1].start_mark.line
            ):
                self.tokens_taken += 1
                c = self.tokens.pop(1)
                self.fetch_more_tokens()
                while len(self.tokens) > 1 and isinstance(self.tokens[1], CommentToken):
                    self.tokens_taken += 1
                    c1 = self.tokens.pop(1)
                    c.value = c.value + (" " * c1.start_mark.column) + c1.value
                    self.fetch_more_tokens()
                self.tokens[0].add_post_comment(c)
            elif (
                len(self.tokens) > 1
                and isinstance(self.tokens[0], ScalarToken)
                and isinstance(self.tokens[1], CommentToken)
                and self.tokens[0].end_mark.line != self.tokens[1].start_mark.line
            ):
                self.tokens_taken += 1
                c = self.tokens.pop(1)
                c.value = (
                    "\n" * (c.start_mark.line - self.tokens[0].end_mark.line)
                    + (" " * c.start_mark.column)
                    + c.value
                )
                self.tokens[0].add_post_comment(c)
                self.fetch_more_tokens()
                while len(self.tokens) > 1 and isinstance(self.tokens[1], CommentToken):
                    self.tokens_taken += 1
                    c1 = self.tokens.pop(1)
                    c.value = c.value + (" " * c1.start_mark.column) + c1.value
                    self.fetch_more_tokens()
            self.tokens_taken += 1
            return self.tokens.pop(0)
        return None

    def fetch_comment(self, comment):
        # type: (Any) -> None
        value, start_mark, end_mark = comment
        while value and value[-1] == " ":
            # empty line within indented key context
            # no need to update end-mark, that is not used
            value = value[:-1]
        self.tokens.append(CommentToken(value, start_mark, end_mark))

    # scanner

    def scan_to_next_token(self):
        # type: () -> Any
        # We ignore spaces, line breaks and comments.
        # If we find a line break in the block context, we set the flag
        # `allow_simple_key` on.
        # The byte order mark is stripped if it's the first character in the
        # stream. We do not yet support BOM inside the stream as the
        # specification requires. Any such mark will be considered as a part
        # of the document.
        #
        # TODO: We need to make tab handling rules more sane. A good rule is
        #   Tabs cannot precede tokens
        #   BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END,
        #   KEY(block), VALUE(block), BLOCK-ENTRY
        # So the checking code is
        #   if <TAB>:
        #       self.allow_simple_keys = False
        # We also need to add the check for `allow_simple_keys == True` to
        # `unwind_indent` before issuing BLOCK-END.
        # Scanners for block, flow, and plain scalars need to be modified.

        srp = self.reader.peek
        srf = self.reader.forward
        if self.reader.index == 0 and srp() == "\uFEFF":
            srf()
        found = False
        while not found:
            while srp() == " ":
                srf()
            ch = srp()
            if ch == "#":
                start_mark = self.reader.get_mark()
                comment = ch
                srf()
                while ch not in _THE_END:
                    ch = srp()
                    if ch == "\0":  # don't gobble the end-of-stream character
                        # but add an explicit newline as "YAML processors should terminate
                        # the stream with an explicit line break
                        # https://yaml.org/spec/1.2/spec.html#id2780069
                        comment += "\n"
                        break
                    comment += ch
                    srf()
                # gather any blank lines following the comment too
                ch = self.scan_line_break()
                while len(ch) > 0:
                    comment += ch
                    ch = self.scan_line_break()
                end_mark = self.reader.get_mark()
                if not self.flow_level:
                    self.allow_simple_key = True
                return comment, start_mark, end_mark
            if bool(self.scan_line_break()):
                start_mark = self.reader.get_mark()
                if not self.flow_level:
                    self.allow_simple_key = True
                ch = srp()
                if ch == "\n":  # empty toplevel lines
                    start_mark = self.reader.get_mark()
                    comment = ""
                    while ch:
                        ch = self.scan_line_break(empty_line=True)
                        comment += ch
                    if srp() == "#":
                        # empty line followed by indented real comment
                        comment = comment.rsplit("\n", 1)[0] + "\n"
                    end_mark = self.reader.get_mark()
                    return comment, start_mark, end_mark
            else:
                found = True
        return None

    def scan_line_break(self, empty_line=False):
        # type: (bool) -> Text
        # Transforms:
        #   '\r\n'      :   '\n'
        #   '\r'        :   '\n'
        #   '\n'        :   '\n'
        #   '\x85'      :   '\n'
        #   '\u2028'    :   '\u2028'
        #   '\u2029     :   '\u2029'
        #   default     :   ''
        ch = self.reader.peek()  # type: Text
        if ch in "\r\n\x85":
            if self.reader.prefix(2) == "\r\n":
                self.reader.forward(2)
            else:
                self.reader.forward()
            return "\n"
        elif ch in "\u2028\u2029":
            self.reader.forward()
            return ch
        elif empty_line and ch in "\t ":
            self.reader.forward()
            return ch
        return ""

    def scan_block_scalar(self, style, rt=True):
        # type: (Any, Optional[bool]) -> Any
        return Scanner.scan_block_scalar(self, style, rt=rt) class Parser(object):
    # Since writing a recursive-descendant parser is a straightforward task, we
    # do not give many comments here.

    DEFAULT_TAGS = {u"!": u"!", u"!!": u"tag:yaml.org,2002:"}

    def __init__(self, loader):
        # type: (Any) -> None
        self.loader = loader
        if self.loader is not None and getattr(self.loader, "_parser", None) is None:
            self.loader._parser = self
        self.reset_parser()

    def reset_parser(self):
        # type: () -> None
        # Reset the state attributes (to clear self-references)
        self.current_event = None
        self.tag_handles = {}  # type: Dict[Any, Any]
        self.states = []  # type: List[Any]
        self.marks = []  # type: List[Any]
        self.state = self.parse_stream_start  # type: Any

    def dispose(self):
        # type: () -> None
        self.reset_parser()

    @property
    def scanner(self):
        # type: () -> Any
        if hasattr(self.loader, "typ"):
            return self.loader.scanner
        return self.loader._scanner

    @property
    def resolver(self):
        # type: () -> Any
        if hasattr(self.loader, "typ"):
            return self.loader.resolver
        return self.loader._resolver

    def check_event(self, *choices):
        # type: (Any) -> bool
        # Check the type of the next event.
        if self.current_event is None:
            if self.state:
                self.current_event = self.state()
        if self.current_event is not None:
            if not choices:
                return True
            for choice in choices:
                if isinstance(self.current_event, choice):
                    return True
        return False

    def peek_event(self):
        # type: () -> Any
        # Get the next event.
        if self.current_event is None:
            if self.state:
                self.current_event = self.state()
        return self.current_event

    def get_event(self):
        # type: () -> Any
        # Get the next event and proceed further.
        if self.current_event is None:
            if self.state:
                self.current_event = self.state()
        value = self.current_event
        self.current_event = None
        return value

    # stream    ::= STREAM-START implicit_document? explicit_document*
    #                                                               STREAM-END
    # implicit_document ::= block_node DOCUMENT-END*
    # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*

    def parse_stream_start(self):
        # type: () -> Any
        # Parse the stream start.
        token = self.scanner.get_token()
        token.move_comment(self.scanner.peek_token())
        event = StreamStartEvent(
            token.start_mark, token.end_mark, encoding=token.encoding
        )

        # Prepare the next state.
        self.state = self.parse_implicit_document_start

        return event

    def parse_implicit_document_start(self):
        # type: () -> Any
        # Parse an implicit document.
        if not self.scanner.check_token(
            DirectiveToken, DocumentStartToken, StreamEndToken
        ):
            self.tag_handles = self.DEFAULT_TAGS
            token = self.scanner.peek_token()
            start_mark = end_mark = token.start_mark
            event = DocumentStartEvent(start_mark, end_mark, explicit=False)

            # Prepare the next state.
            self.states.append(self.parse_document_end)
            self.state = self.parse_block_node

            return event

        else:
            return self.parse_document_start()

    def parse_document_start(self):
        # type: () -> Any
        # Parse any extra document end indicators.
        while self.scanner.check_token(DocumentEndToken):
            self.scanner.get_token()
        # Parse an explicit document.
        if not self.scanner.check_token(StreamEndToken):
            token = self.scanner.peek_token()
            start_mark = token.start_mark
            version, tags = self.process_directives()
            if not self.scanner.check_token(DocumentStartToken):
                raise ParserError(
                    None,
                    None,
                    "expected '<document start>', but found %r"
                    % self.scanner.peek_token().id,
                    self.scanner.peek_token().start_mark,
                )
            token = self.scanner.get_token()
            end_mark = token.end_mark
            # if self.loader is not None and \
            #    end_mark.line != self.scanner.peek_token().start_mark.line:
            #     self.loader.scalar_after_indicator = False
            event = DocumentStartEvent(
                start_mark, end_mark, explicit=True, version=version, tags=tags
            )  # type: Any
            self.states.append(self.parse_document_end)
            self.state = self.parse_document_content
        else:
            # Parse the end of the stream.
            token = self.scanner.get_token()
            event = StreamEndEvent(
                token.start_mark, token.end_mark, comment=token.comment
            )
            assert not self.states
            assert not self.marks
            self.state = None
        return event

    def parse_document_end(self):
        # type: () -> Any
        # Parse the document end.
        token = self.scanner.peek_token()
        start_mark = end_mark = token.start_mark
        explicit = False
        if self.scanner.check_token(DocumentEndToken):
            token = self.scanner.get_token()
            end_mark = token.end_mark
            explicit = True
        event = DocumentEndEvent(start_mark, end_mark, explicit=explicit)

        # Prepare the next state.
        if self.resolver.processing_version == (1, 1):
            self.state = self.parse_document_start
        else:
            self.state = self.parse_implicit_document_start

        return event

    def parse_document_content(self):
        # type: () -> Any
        if self.scanner.check_token(
            DirectiveToken, DocumentStartToken, DocumentEndToken, StreamEndToken
        ):
            event = self.process_empty_scalar(self.scanner.peek_token().start_mark)
            self.state = self.states.pop()
            return event
        else:
            return self.parse_block_node()

    def process_directives(self):
        # type: () -> Any
        yaml_version = None
        self.tag_handles = {}
        while self.scanner.check_token(DirectiveToken):
            token = self.scanner.get_token()
            if token.name == u"YAML":
                if yaml_version is not None:
                    raise ParserError(
                        None, None, "found duplicate YAML directive", token.start_mark
                    )
                major, minor = token.value
                if major != 1:
                    raise ParserError(
                        None,
                        None,
                        "found incompatible YAML document (version 1.* is " "required)",
                        token.start_mark,
                    )
                yaml_version = token.value
            elif token.name == u"TAG":
                handle, prefix = token.value
                if handle in self.tag_handles:
                    raise ParserError(
                        None,
                        None,
                        "duplicate tag handle %r" % utf8(handle),
                        token.start_mark,
                    )
                self.tag_handles[handle] = prefix
        if bool(self.tag_handles):
            value = yaml_version, self.tag_handles.copy()  # type: Any
        else:
            value = yaml_version, None
        if self.loader is not None and hasattr(self.loader, "tags"):
            self.loader.version = yaml_version
            if self.loader.tags is None:
                self.loader.tags = {}
            for k in self.tag_handles:
                self.loader.tags[k] = self.tag_handles[k]
        for key in self.DEFAULT_TAGS:
            if key not in self.tag_handles:
                self.tag_handles[key] = self.DEFAULT_TAGS[key]
        return value

    # block_node_or_indentless_sequence ::= ALIAS
    #               | properties (block_content | indentless_block_sequence)?
    #               | block_content
    #               | indentless_block_sequence
    # block_node    ::= ALIAS
    #                   | properties block_content?
    #                   | block_content
    # flow_node     ::= ALIAS
    #                   | properties flow_content?
    #                   | flow_content
    # properties    ::= TAG ANCHOR? | ANCHOR TAG?
    # block_content     ::= block_collection | flow_collection | SCALAR
    # flow_content      ::= flow_collection | SCALAR
    # block_collection  ::= block_sequence | block_mapping
    # flow_collection   ::= flow_sequence | flow_mapping

    def parse_block_node(self):
        # type: () -> Any
        return self.parse_node(block=True)

    def parse_flow_node(self):
        # type: () -> Any
        return self.parse_node()

    def parse_block_node_or_indentless_sequence(self):
        # type: () -> Any
        return self.parse_node(block=True, indentless_sequence=True)

    def transform_tag(self, handle, suffix):
        # type: (Any, Any) -> Any
        return self.tag_handles[handle] + suffix

    def parse_node(self, block=False, indentless_sequence=False):
        # type: (bool, bool) -> Any
        if self.scanner.check_token(AliasToken):
            token = self.scanner.get_token()
            event = AliasEvent(
                token.value, token.start_mark, token.end_mark
            )  # type: Any
            self.state = self.states.pop()
            return event

        anchor = None
        tag = None
        start_mark = end_mark = tag_mark = None
        if self.scanner.check_token(AnchorToken):
            token = self.scanner.get_token()
            start_mark = token.start_mark
            end_mark = token.end_mark
            anchor = token.value
            if self.scanner.check_token(TagToken):
                token = self.scanner.get_token()
                tag_mark = token.start_mark
                end_mark = token.end_mark
                tag = token.value
        elif self.scanner.check_token(TagToken):
            token = self.scanner.get_token()
            start_mark = tag_mark = token.start_mark
            end_mark = token.end_mark
            tag = token.value
            if self.scanner.check_token(AnchorToken):
                token = self.scanner.get_token()
                start_mark = tag_mark = token.start_mark
                end_mark = token.end_mark
                anchor = token.value
        if tag is not None:
            handle, suffix = tag
            if handle is not None:
                if handle not in self.tag_handles:
                    raise ParserError(
                        "while parsing a node",
                        start_mark,
                        "found undefined tag handle %r" % utf8(handle),
                        tag_mark,
                    )
                tag = self.transform_tag(handle, suffix)
            else:
                tag = suffix
        # if tag == u'!':
        #     raise ParserError("while parsing a node", start_mark,
        #             "found non-specific tag '!'", tag_mark,
        #      "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag'
        #     and share your opinion.")
        if start_mark is None:
            start_mark = end_mark = self.scanner.peek_token().start_mark
        event = None
        implicit = tag is None or tag == u"!"
        if indentless_sequence and self.scanner.check_token(BlockEntryToken):
            comment = None
            pt = self.scanner.peek_token()
            if pt.comment and pt.comment[0]:
                comment = [pt.comment[0], []]
                pt.comment[0] = None
            end_mark = self.scanner.peek_token().end_mark
            event = SequenceStartEvent(
                anchor,
                tag,
                implicit,
                start_mark,
                end_mark,
                flow_style=False,
                comment=comment,
            )
            self.state = self.parse_indentless_sequence_entry
            return event

        if self.scanner.check_token(ScalarToken):
            token = self.scanner.get_token()
            # self.scanner.peek_token_same_line_comment(token)
            end_mark = token.end_mark
            if (token.plain and tag is None) or tag == u"!":
                implicit = (True, False)
            elif tag is None:
                implicit = (False, True)
            else:
                implicit = (False, False)
            # nprint('se', token.value, token.comment)
            event = ScalarEvent(
                anchor,
                tag,
                implicit,
                token.value,
                start_mark,
                end_mark,
                style=token.style,
                comment=token.comment,
            )
            self.state = self.states.pop()
        elif self.scanner.check_token(FlowSequenceStartToken):
            pt = self.scanner.peek_token()
            end_mark = pt.end_mark
            event = SequenceStartEvent(
                anchor,
                tag,
                implicit,
                start_mark,
                end_mark,
                flow_style=True,
                comment=pt.comment,
            )
            self.state = self.parse_flow_sequence_first_entry
        elif self.scanner.check_token(FlowMappingStartToken):
            pt = self.scanner.peek_token()
            end_mark = pt.end_mark
            event = MappingStartEvent(
                anchor,
                tag,
                implicit,
                start_mark,
                end_mark,
                flow_style=True,
                comment=pt.comment,
            )
            self.state = self.parse_flow_mapping_first_key
        elif block and self.scanner.check_token(BlockSequenceStartToken):
            end_mark = self.scanner.peek_token().start_mark
            # should inserting the comment be dependent on the
            # indentation?
            pt = self.scanner.peek_token()
            comment = pt.comment
            # nprint('pt0', type(pt))
            if comment is None or comment[1] is None:
                comment = pt.split_comment()
            # nprint('pt1', comment)
            event = SequenceStartEvent(
                anchor,
                tag,
                implicit,
                start_mark,
                end_mark,
                flow_style=False,
                comment=comment,
            )
            self.state = self.parse_block_sequence_first_entry
        elif block and self.scanner.check_token(BlockMappingStartToken):
            end_mark = self.scanner.peek_token().start_mark
            comment = self.scanner.peek_token().comment
            event = MappingStartEvent(
                anchor,
                tag,
                implicit,
                start_mark,
                end_mark,
                flow_style=False,
                comment=comment,
            )
            self.state = self.parse_block_mapping_first_key
        elif anchor is not None or tag is not None:
            # Empty scalars are allowed even if a tag or an anchor is
            # specified.
            event = ScalarEvent(
                anchor, tag, (implicit, False), "", start_mark, end_mark
            )
            self.state = self.states.pop()
        else:
            if block:
                node = "block"
            else:
                node = "flow"
            token = self.scanner.peek_token()
            raise ParserError(
                "while parsing a %s node" % node,
                start_mark,
                "expected the node content, but found %r" % token.id,
                token.start_mark,
            )
        return event

    # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)*
    #                                                               BLOCK-END

    def parse_block_sequence_first_entry(self):
        # type: () -> Any
        token = self.scanner.get_token()
        # move any comment from start token
        # token.move_comment(self.scanner.peek_token())
        self.marks.append(token.start_mark)
        return self.parse_block_sequence_entry()

    def parse_block_sequence_entry(self):
        # type: () -> Any
        if self.scanner.check_token(BlockEntryToken):
            token = self.scanner.get_token()
            token.move_comment(self.scanner.peek_token())
            if not self.scanner.check_token(BlockEntryToken, BlockEndToken):
                self.states.append(self.parse_block_sequence_entry)
                return self.parse_block_node()
            else:
                self.state = self.parse_block_sequence_entry
                return self.process_empty_scalar(token.end_mark)
        if not self.scanner.check_token(BlockEndToken):
            token = self.scanner.peek_token()
            raise ParserError(
                "while parsing a block collection",
                self.marks[-1],
                "expected <block end>, but found %r" % token.id,
                token.start_mark,
            )
        token = self.scanner.get_token()  # BlockEndToken
        event = SequenceEndEvent(
            token.start_mark, token.end_mark, comment=token.comment
        )
        self.state = self.states.pop()
        self.marks.pop()
        return event

    # indentless_sequence ::= (BLOCK-ENTRY block_node?)+

    # indentless_sequence?
    # sequence:
    # - entry
    #  - nested

    def parse_indentless_sequence_entry(self):
        # type: () -> Any
        if self.scanner.check_token(BlockEntryToken):
            token = self.scanner.get_token()
            token.move_comment(self.scanner.peek_token())
            if not self.scanner.check_token(
                BlockEntryToken, KeyToken, ValueToken, BlockEndToken
            ):
                self.states.append(self.parse_indentless_sequence_entry)
                return self.parse_block_node()
            else:
                self.state = self.parse_indentless_sequence_entry
                return self.process_empty_scalar(token.end_mark)
        token = self.scanner.peek_token()
        event = SequenceEndEvent(
            token.start_mark, token.start_mark, comment=token.comment
        )
        self.state = self.states.pop()
        return event

    # block_mapping     ::= BLOCK-MAPPING_START
    #                       ((KEY block_node_or_indentless_sequence?)?
    #                       (VALUE block_node_or_indentless_sequence?)?)*
    #                       BLOCK-END

    def parse_block_mapping_first_key(self):
        # type: () -> Any
        token = self.scanner.get_token()
        self.marks.append(token.start_mark)
        return self.parse_block_mapping_key()

    def parse_block_mapping_key(self):
        # type: () -> Any
        if self.scanner.check_token(KeyToken):
            token = self.scanner.get_token()
            token.move_comment(self.scanner.peek_token())
            if not self.scanner.check_token(KeyToken, ValueToken, BlockEndToken):
                self.states.append(self.parse_block_mapping_value)
                return self.parse_block_node_or_indentless_sequence()
            else:
                self.state = self.parse_block_mapping_value
                return self.process_empty_scalar(token.end_mark)
        if self.resolver.processing_version > (1, 1) and self.scanner.check_token(
            ValueToken
        ):
            self.state = self.parse_block_mapping_value
            return self.process_empty_scalar(self.scanner.peek_token().start_mark)
        if not self.scanner.check_token(BlockEndToken):
            token = self.scanner.peek_token()
            raise ParserError(
                "while parsing a block mapping",
                self.marks[-1],
                "expected <block end>, but found %r" % token.id,
                token.start_mark,
            )
        token = self.scanner.get_token()
        token.move_comment(self.scanner.peek_token())
        event = MappingEndEvent(token.start_mark, token.end_mark, comment=token.comment)
        self.state = self.states.pop()
        self.marks.pop()
        return event

    def parse_block_mapping_value(self):
        # type: () -> Any
        if self.scanner.check_token(ValueToken):
            token = self.scanner.get_token()
            # value token might have post comment move it to e.g. block
            if self.scanner.check_token(ValueToken):
                token.move_comment(self.scanner.peek_token())
            else:
                if not self.scanner.check_token(KeyToken):
                    token.move_comment(self.scanner.peek_token(), empty=True)
                # else: empty value for this key cannot move token.comment
            if not self.scanner.check_token(KeyToken, ValueToken, BlockEndToken):
                self.states.append(self.parse_block_mapping_key)
                return self.parse_block_node_or_indentless_sequence()
            else:
                self.state = self.parse_block_mapping_key
                comment = token.comment
                if comment is None:
                    token = self.scanner.peek_token()
                    comment = token.comment
                    if comment:
                        token._comment = [None, comment[1]]
                        comment = [comment[0], None]
                return self.process_empty_scalar(token.end_mark, comment=comment)
        else:
            self.state = self.parse_block_mapping_key
            token = self.scanner.peek_token()
            return self.process_empty_scalar(token.start_mark)

    # flow_sequence     ::= FLOW-SEQUENCE-START
    #                       (flow_sequence_entry FLOW-ENTRY)*
    #                       flow_sequence_entry?
    #                       FLOW-SEQUENCE-END
    # flow_sequence_entry   ::= flow_node | KEY flow_node? (VALUE flow_node?)?
    #
    # Note that while production rules for both flow_sequence_entry and
    # flow_mapping_entry are equal, their interpretations are different.
    # For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?`
    # generate an inline mapping (set syntax).

    def parse_flow_sequence_first_entry(self):
        # type: () -> Any
        token = self.scanner.get_token()
        self.marks.append(token.start_mark)
        return self.parse_flow_sequence_entry(first=True)

    def parse_flow_sequence_entry(self, first=False):
        # type: (bool) -> Any
        if not self.scanner.check_token(FlowSequenceEndToken):
            if not first:
                if self.scanner.check_token(FlowEntryToken):
                    self.scanner.get_token()
                else:
                    token = self.scanner.peek_token()
                    raise ParserError(
                        "while parsing a flow sequence",
                        self.marks[-1],
                        "expected ',' or ']', but got %r" % token.id,
                        token.start_mark,
                    )

            if self.scanner.check_token(KeyToken):
                token = self.scanner.peek_token()
                event = MappingStartEvent(
                    None, None, True, token.start_mark, token.end_mark, flow_style=True
                )  # type: Any
                self.state = self.parse_flow_sequence_entry_mapping_key
                return event
            elif not self.scanner.check_token(FlowSequenceEndToken):
                self.states.append(self.parse_flow_sequence_entry)
                return self.parse_flow_node()
        token = self.scanner.get_token()
        event = SequenceEndEvent(
            token.start_mark, token.end_mark, comment=token.comment
        )
        self.state = self.states.pop()
        self.marks.pop()
        return event

    def parse_flow_sequence_entry_mapping_key(self):
        # type: () -> Any
        token = self.scanner.get_token()
        if not self.scanner.check_token(
            ValueToken, FlowEntryToken, FlowSequenceEndToken
        ):
            self.states.append(self.parse_flow_sequence_entry_mapping_value)
            return self.parse_flow_node()
        else:
            self.state = self.parse_flow_sequence_entry_mapping_value
            return self.process_empty_scalar(token.end_mark)

    def parse_flow_sequence_entry_mapping_value(self):
        # type: () -> Any
        if self.scanner.check_token(ValueToken):
            token = self.scanner.get_token()
            if not self.scanner.check_token(FlowEntryToken, FlowSequenceEndToken):
                self.states.append(self.parse_flow_sequence_entry_mapping_end)
                return self.parse_flow_node()
            else:
                self.state = self.parse_flow_sequence_entry_mapping_end
                return self.process_empty_scalar(token.end_mark)
        else:
            self.state = self.parse_flow_sequence_entry_mapping_end
            token = self.scanner.peek_token()
            return self.process_empty_scalar(token.start_mark)

    def parse_flow_sequence_entry_mapping_end(self):
        # type: () -> Any
        self.state = self.parse_flow_sequence_entry
        token = self.scanner.peek_token()
        return MappingEndEvent(token.start_mark, token.start_mark)

    # flow_mapping  ::= FLOW-MAPPING-START
    #                   (flow_mapping_entry FLOW-ENTRY)*
    #                   flow_mapping_entry?
    #                   FLOW-MAPPING-END
    # flow_mapping_entry    ::= flow_node | KEY flow_node? (VALUE flow_node?)?

    def parse_flow_mapping_first_key(self):
        # type: () -> Any
        token = self.scanner.get_token()
        self.marks.append(token.start_mark)
        return self.parse_flow_mapping_key(first=True)

    def parse_flow_mapping_key(self, first=False):
        # type: (Any) -> Any
        if not self.scanner.check_token(FlowMappingEndToken):
            if not first:
                if self.scanner.check_token(FlowEntryToken):
                    self.scanner.get_token()
                else:
                    token = self.scanner.peek_token()
                    raise ParserError(
                        "while parsing a flow mapping",
                        self.marks[-1],
                        "expected ',' or '}', but got %r" % token.id,
                        token.start_mark,
                    )
            if self.scanner.check_token(KeyToken):
                token = self.scanner.get_token()
                if not self.scanner.check_token(
                    ValueToken, FlowEntryToken, FlowMappingEndToken
                ):
                    self.states.append(self.parse_flow_mapping_value)
                    return self.parse_flow_node()
                else:
                    self.state = self.parse_flow_mapping_value
                    return self.process_empty_scalar(token.end_mark)
            elif self.resolver.processing_version > (1, 1) and self.scanner.check_token(
                ValueToken
            ):
                self.state = self.parse_flow_mapping_value
                return self.process_empty_scalar(self.scanner.peek_token().end_mark)
            elif not self.scanner.check_token(FlowMappingEndToken):
                self.states.append(self.parse_flow_mapping_empty_value)
                return self.parse_flow_node()
        token = self.scanner.get_token()
        event = MappingEndEvent(token.start_mark, token.end_mark, comment=token.comment)
        self.state = self.states.pop()
        self.marks.pop()
        return event

    def parse_flow_mapping_value(self):
        # type: () -> Any
        if self.scanner.check_token(ValueToken):
            token = self.scanner.get_token()
            if not self.scanner.check_token(FlowEntryToken, FlowMappingEndToken):
                self.states.append(self.parse_flow_mapping_key)
                return self.parse_flow_node()
            else:
                self.state = self.parse_flow_mapping_key
                return self.process_empty_scalar(token.end_mark)
        else:
            self.state = self.parse_flow_mapping_key
            token = self.scanner.peek_token()
            return self.process_empty_scalar(token.start_mark)

    def parse_flow_mapping_empty_value(self):
        # type: () -> Any
        self.state = self.parse_flow_mapping_key
        return self.process_empty_scalar(self.scanner.peek_token().start_mark)

    def process_empty_scalar(self, mark, comment=None):
        # type: (Any, Any) -> Any
        return ScalarEvent(None, None, (True, False), "", mark, mark, comment=comment) class RoundTripParser(Parser):
    """roundtrip is a safe loader, that wants to see the unmangled tag"""

    def transform_tag(self, handle, suffix):
        # type: (Any, Any) -> Any
        # return self.tag_handles[handle]+suffix
        if handle == "!!" and suffix in (
            u"null",
            u"bool",
            u"int",
            u"float",
            u"binary",
            u"timestamp",
            u"omap",
            u"pairs",
            u"set",
            u"str",
            u"seq",
            u"map",
        ):
            return Parser.transform_tag(self, handle, suffix)
        return handle + suffix class Composer(object):
    def __init__(self, loader=None):
        # type: (Any) -> None
        self.loader = loader
        if self.loader is not None and getattr(self.loader, "_composer", None) is None:
            self.loader._composer = self
        self.anchors = {}  # type: Dict[Any, Any]

    @property
    def parser(self):
        # type: () -> Any
        if hasattr(self.loader, "typ"):
            self.loader.parser
        return self.loader._parser

    @property
    def resolver(self):
        # type: () -> Any
        # assert self.loader._resolver is not None
        if hasattr(self.loader, "typ"):
            self.loader.resolver
        return self.loader._resolver

    def check_node(self):
        # type: () -> Any
        # Drop the STREAM-START event.
        if self.parser.check_event(StreamStartEvent):
            self.parser.get_event()

        # If there are more documents available?
        return not self.parser.check_event(StreamEndEvent)

    def get_node(self):
        # type: () -> Any
        # Get the root node of the next document.
        if not self.parser.check_event(StreamEndEvent):
            return self.compose_document()

    def get_single_node(self):
        # type: () -> Any
        # Drop the STREAM-START event.
        self.parser.get_event()

        # Compose a document if the stream is not empty.
        document = None  # type: Any
        if not self.parser.check_event(StreamEndEvent):
            document = self.compose_document()

        # Ensure that the stream contains no more documents.
        if not self.parser.check_event(StreamEndEvent):
            event = self.parser.get_event()
            raise ComposerError(
                "expected a single document in the stream",
                document.start_mark,
                "but found another document",
                event.start_mark,
            )

        # Drop the STREAM-END event.
        self.parser.get_event()

        return document

    def compose_document(self):
        # type: (Any) -> Any
        # Drop the DOCUMENT-START event.
        self.parser.get_event()

        # Compose the root node.
        node = self.compose_node(None, None)

        # Drop the DOCUMENT-END event.
        self.parser.get_event()

        self.anchors = {}
        return node

    def compose_node(self, parent, index):
        # type: (Any, Any) -> Any
        if self.parser.check_event(AliasEvent):
            event = self.parser.get_event()
            alias = event.anchor
            if alias not in self.anchors:
                raise ComposerError(
                    None,
                    None,
                    "found undefined alias %r" % utf8(alias),
                    event.start_mark,
                )
            return self.anchors[alias]
        event = self.parser.peek_event()
        anchor = event.anchor
        if anchor is not None:  # have an anchor
            if anchor in self.anchors:
                # raise ComposerError(
                #     "found duplicate anchor %r; first occurrence"
                #     % utf8(anchor), self.anchors[anchor].start_mark,
                #     "second occurrence", event.start_mark)
                ws = (
                    "\nfound duplicate anchor {!r}\nfirst occurrence {}\nsecond occurrence "
                    "{}".format(
                        (anchor), self.anchors[anchor].start_mark, event.start_mark
                    )
                )
                warnings.warn(ws, ReusedAnchorWarning)
        self.resolver.descend_resolver(parent, index)
        if self.parser.check_event(ScalarEvent):
            node = self.compose_scalar_node(anchor)
        elif self.parser.check_event(SequenceStartEvent):
            node = self.compose_sequence_node(anchor)
        elif self.parser.check_event(MappingStartEvent):
            node = self.compose_mapping_node(anchor)
        self.resolver.ascend_resolver()
        return node

    def compose_scalar_node(self, anchor):
        # type: (Any) -> Any
        event = self.parser.get_event()
        tag = event.tag
        if tag is None or tag == u"!":
            tag = self.resolver.resolve(ScalarNode, event.value, event.implicit)
        node = ScalarNode(
            tag,
            event.value,
            event.start_mark,
            event.end_mark,
            style=event.style,
            comment=event.comment,
            anchor=anchor,
        )
        if anchor is not None:
            self.anchors[anchor] = node
        return node

    def compose_sequence_node(self, anchor):
        # type: (Any) -> Any
        start_event = self.parser.get_event()
        tag = start_event.tag
        if tag is None or tag == u"!":
            tag = self.resolver.resolve(SequenceNode, None, start_event.implicit)
        node = SequenceNode(
            tag,
            [],
            start_event.start_mark,
            None,
            flow_style=start_event.flow_style,
            comment=start_event.comment,
            anchor=anchor,
        )
        if anchor is not None:
            self.anchors[anchor] = node
        index = 0
        while not self.parser.check_event(SequenceEndEvent):
            node.value.append(self.compose_node(node, index))
            index += 1
        end_event = self.parser.get_event()
        if node.flow_style is True and end_event.comment is not None:
            if node.comment is not None:
                nprint(
                    "Warning: unexpected end_event commment in sequence "
                    "node {}".format(node.flow_style)
                )
            node.comment = end_event.comment
        node.end_mark = end_event.end_mark
        self.check_end_doc_comment(end_event, node)
        return node

    def compose_mapping_node(self, anchor):
        # type: (Any) -> Any
        start_event = self.parser.get_event()
        tag = start_event.tag
        if tag is None or tag == u"!":
            tag = self.resolver.resolve(MappingNode, None, start_event.implicit)
        node = MappingNode(
            tag,
            [],
            start_event.start_mark,
            None,
            flow_style=start_event.flow_style,
            comment=start_event.comment,
            anchor=anchor,
        )
        if anchor is not None:
            self.anchors[anchor] = node
        while not self.parser.check_event(MappingEndEvent):
            # key_event = self.parser.peek_event()
            item_key = self.compose_node(node, None)
            # if item_key in node.value:
            #     raise ComposerError("while composing a mapping",
            #             start_event.start_mark,
            #             "found duplicate key", key_event.start_mark)
            item_value = self.compose_node(node, item_key)
            # node.value[item_key] = item_value
            node.value.append((item_key, item_value))
        end_event = self.parser.get_event()
        if node.flow_style is True and end_event.comment is not None:
            node.comment = end_event.comment
        node.end_mark = end_event.end_mark
        self.check_end_doc_comment(end_event, node)
        return node

    def check_end_doc_comment(self, end_event, node):
        # type: (Any, Any) -> None
        if end_event.comment and end_event.comment[1]:
            # pre comments on an end_event, no following to move to
            if node.comment is None:
                node.comment = [None, None]
            assert not isinstance(node, ScalarEvent)
            # this is a post comment on a mapping node, add as third element
            # in the list
            node.comment.append(end_event.comment[1])
            end_event.comment[1] = None class BaseConstructor(object):

    yaml_constructors = {}  # type: Dict[Any, Any]
    yaml_multi_constructors = {}  # type: Dict[Any, Any]

    def __init__(self, preserve_quotes=None, loader=None):
        # type: (Optional[bool], Any) -> None
        self.loader = loader
        if (
            self.loader is not None
            and getattr(self.loader, "_constructor", None) is None
        ):
            self.loader._constructor = self
        self.loader = loader
        self.yaml_base_dict_type = dict
        self.yaml_base_list_type = list
        self.constructed_objects = {}  # type: Dict[Any, Any]
        self.recursive_objects = {}  # type: Dict[Any, Any]
        self.state_generators = []  # type: List[Any]
        self.deep_construct = False
        self._preserve_quotes = preserve_quotes
        self.allow_duplicate_keys = version_tnf((0, 15, 1), (0, 16))

    @property
    def composer(self):
        # type: () -> Any
        if hasattr(self.loader, "typ"):
            return self.loader.composer
        try:
            return self.loader._composer
        except AttributeError:
            sys.stdout.write("slt {}\n".format(type(self)))
            sys.stdout.write("slc {}\n".format(self.loader._composer))
            sys.stdout.write("{}\n".format(dir(self)))
            raise

    @property
    def resolver(self):
        # type: () -> Any
        if hasattr(self.loader, "typ"):
            return self.loader.resolver
        return self.loader._resolver

    def check_data(self):
        # type: () -> Any
        # If there are more documents available?
        return self.composer.check_node()

    def get_data(self):
        # type: () -> Any
        # Construct and return the next document.
        if self.composer.check_node():
            return self.construct_document(self.composer.get_node())

    def get_single_data(self):
        # type: () -> Any
        # Ensure that the stream contains a single document and construct it.
        node = self.composer.get_single_node()
        if node is not None:
            return self.construct_document(node)
        return None

    def construct_document(self, node):
        # type: (Any) -> Any
        data = self.construct_object(node)
        while bool(self.state_generators):
            state_generators = self.state_generators
            self.state_generators = []
            for generator in state_generators:
                for _dummy in generator:
                    pass
        self.constructed_objects = {}
        self.recursive_objects = {}
        self.deep_construct = False
        return data

    def construct_object(self, node, deep=False):
        # type: (Any, bool) -> Any
        """deep is True when creating an object/mapping recursively,
        in that case want the underlying elements available during construction
        """
        if node in self.constructed_objects:
            return self.constructed_objects[node]
        if deep:
            old_deep = self.deep_construct
            self.deep_construct = True
        if node in self.recursive_objects:
            return self.recursive_objects[node]
            # raise ConstructorError(
            #     None, None, 'found unconstructable recursive node', node.start_mark
            # )
        self.recursive_objects[node] = None
        data = self.construct_non_recursive_object(node)

        self.constructed_objects[node] = data
        del self.recursive_objects[node]
        if deep:
            self.deep_construct = old_deep
        return data

    def construct_non_recursive_object(self, node, tag=None):
        # type: (Any, Optional[str]) -> Any
        constructor = None  # type: Any
        tag_suffix = None
        if tag is None:
            tag = node.tag
        if tag in self.yaml_constructors:
            constructor = self.yaml_constructors[tag]
        else:
            for tag_prefix in self.yaml_multi_constructors:
                if tag.startswith(tag_prefix):
                    tag_suffix = tag[len(tag_prefix) :]
                    constructor = self.yaml_multi_constructors[tag_prefix]
                    break
            else:
                if None in self.yaml_multi_constructors:
                    tag_suffix = tag
                    constructor = self.yaml_multi_constructors[None]
                elif None in self.yaml_constructors:
                    constructor = self.yaml_constructors[None]
                elif isinstance(node, ScalarNode):
                    constructor = self.__class__.construct_scalar
                elif isinstance(node, SequenceNode):
                    constructor = self.__class__.construct_sequence
                elif isinstance(node, MappingNode):
                    constructor = self.__class__.construct_mapping
        if tag_suffix is None:
            data = constructor(self, node)
        else:
            data = constructor(self, tag_suffix, node)
        if isinstance(data, types.GeneratorType):
            generator = data
            data = next(generator)
            if self.deep_construct:
                for _dummy in generator:
                    pass
            else:
                self.state_generators.append(generator)
        return data

    def construct_scalar(self, node):
        # type: (Any) -> Any
        if not isinstance(node, ScalarNode):
            raise ConstructorError(
                None,
                None,
                "expected a scalar node, but found %s" % node.id,
                node.start_mark,
            )
        return node.value

    def construct_sequence(self, node, deep=False):
        # type: (Any, bool) -> Any
        """deep is True when creating an object/mapping recursively,
        in that case want the underlying elements available during construction
        """
        if not isinstance(node, SequenceNode):
            raise ConstructorError(
                None,
                None,
                "expected a sequence node, but found %s" % node.id,
                node.start_mark,
            )
        return [self.construct_object(child, deep=deep) for child in node.value]

    def construct_mapping(self, node, deep=False):
        # type: (Any, bool) -> Any
        """deep is True when creating an object/mapping recursively,
        in that case want the underlying elements available during construction
        """
        if not isinstance(node, MappingNode):
            raise ConstructorError(
                None,
                None,
                "expected a mapping node, but found %s" % node.id,
                node.start_mark,
            )
        total_mapping = self.yaml_base_dict_type()
        if getattr(node, "merge", None) is not None:
            todo = [(node.merge, False), (node.value, False)]
        else:
            todo = [(node.value, True)]
        for values, check in todo:
            mapping = self.yaml_base_dict_type()  # type: Dict[Any, Any]
            for key_node, value_node in values:
                # keys can be list -> deep
                key = self.construct_object(key_node, deep=True)
                # lists are not hashable, but tuples are
                if not isinstance(key, Hashable):
                    if isinstance(key, list):
                        key = tuple(key)
                if PY2:
                    try:
                        hash(key)
                    except TypeError as exc:
                        raise ConstructorError(
                            "while constructing a mapping",
                            node.start_mark,
                            "found unacceptable key (%s)" % exc,
                            key_node.start_mark,
                        )
                else:
                    if not isinstance(key, Hashable):
                        raise ConstructorError(
                            "while constructing a mapping",
                            node.start_mark,
                            "found unhashable key",
                            key_node.start_mark,
                        )

                value = self.construct_object(value_node, deep=deep)
                if check:
                    if self.check_mapping_key(node, key_node, mapping, key, value):
                        mapping[key] = value
                else:
                    mapping[key] = value
            total_mapping.update(mapping)
        return total_mapping

    def check_mapping_key(self, node, key_node, mapping, key, value):
        # type: (Any, Any, Any, Any, Any) -> bool
        """return True if key is unique"""
        if key in mapping:
            if not self.allow_duplicate_keys:
                mk = mapping.get(key)
                if PY2:
                    if isinstance(key, unicode):
                        key = key.encode("utf-8")
                    if isinstance(value, unicode):
                        value = value.encode("utf-8")
                    if isinstance(mk, unicode):
                        mk = mk.encode("utf-8")
                args = [
                    "while constructing a mapping",
                    node.start_mark,
                    'found duplicate key "{}" with value "{}" '
                    '(original value: "{}")'.format(key, value, mk),
                    key_node.start_mark,
                    """
                    To suppress this check see:
                        http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys
                    """,
                    """\
                    Duplicate keys will become an error in future releases, and are errors
                    by default when using the new API.
                    """,
                ]
                if self.allow_duplicate_keys is None:
                    warnings.warn(DuplicateKeyFutureWarning(*args))
                else:
                    raise DuplicateKeyError(*args)
            return False
        return True

    def check_set_key(self, node, key_node, setting, key):
        # type: (Any, Any, Any, Any, Any) -> None
        if key in setting:
            if not self.allow_duplicate_keys:
                if PY2:
                    if isinstance(key, unicode):
                        key = key.encode("utf-8")
                args = [
                    "while constructing a set",
                    node.start_mark,
                    'found duplicate key "{}"'.format(key),
                    key_node.start_mark,
                    """
                    To suppress this check see:
                        http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys
                    """,
                    """\
                    Duplicate keys will become an error in future releases, and are errors
                    by default when using the new API.
                    """,
                ]
                if self.allow_duplicate_keys is None:
                    warnings.warn(DuplicateKeyFutureWarning(*args))
                else:
                    raise DuplicateKeyError(*args)

    def construct_pairs(self, node, deep=False):
        # type: (Any, bool) -> Any
        if not isinstance(node, MappingNode):
            raise ConstructorError(
                None,
                None,
                "expected a mapping node, but found %s" % node.id,
                node.start_mark,
            )
        pairs = []
        for key_node, value_node in node.value:
            key = self.construct_object(key_node, deep=deep)
            value = self.construct_object(value_node, deep=deep)
            pairs.append((key, value))
        return pairs

    @classmethod
    def add_constructor(cls, tag, constructor):
        # type: (Any, Any) -> None
        if "yaml_constructors" not in cls.__dict__:
            cls.yaml_constructors = cls.yaml_constructors.copy()
        cls.yaml_constructors[tag] = constructor

    @classmethod
    def add_multi_constructor(cls, tag_prefix, multi_constructor):
        # type: (Any, Any) -> None
        if "yaml_multi_constructors" not in cls.__dict__:
            cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy()
        cls.yaml_multi_constructors[tag_prefix] = multi_constructor class SafeConstructor(BaseConstructor):
    def construct_scalar(self, node):
        # type: (Any) -> Any
        if isinstance(node, MappingNode):
            for key_node, value_node in node.value:
                if key_node.tag == u"tag:yaml.org,2002:value":
                    return self.construct_scalar(value_node)
        return BaseConstructor.construct_scalar(self, node)

    def flatten_mapping(self, node):
        # type: (Any) -> Any
        """
        This implements the merge key feature http://yaml.org/type/merge.html
        by inserting keys from the merge dict/list of dicts if not yet
        available in this node
        """
        merge = []  # type: List[Any]
        index = 0
        while index < len(node.value):
            key_node, value_node = node.value[index]
            if key_node.tag == u"tag:yaml.org,2002:merge":
                if merge:  # double << key
                    if self.allow_duplicate_keys:
                        del node.value[index]
                        index += 1
                        continue
                    args = [
                        "while constructing a mapping",
                        node.start_mark,
                        'found duplicate key "{}"'.format(key_node.value),
                        key_node.start_mark,
                        """
                        To suppress this check see:
                           http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys
                        """,
                        """\
                        Duplicate keys will become an error in future releases, and are errors
                        by default when using the new API.
                        """,
                    ]
                    if self.allow_duplicate_keys is None:
                        warnings.warn(DuplicateKeyFutureWarning(*args))
                    else:
                        raise DuplicateKeyError(*args)
                del node.value[index]
                if isinstance(value_node, MappingNode):
                    self.flatten_mapping(value_node)
                    merge.extend(value_node.value)
                elif isinstance(value_node, SequenceNode):
                    submerge = []
                    for subnode in value_node.value:
                        if not isinstance(subnode, MappingNode):
                            raise ConstructorError(
                                "while constructing a mapping",
                                node.start_mark,
                                "expected a mapping for merging, but found %s"
                                % subnode.id,
                                subnode.start_mark,
                            )
                        self.flatten_mapping(subnode)
                        submerge.append(subnode.value)
                    submerge.reverse()
                    for value in submerge:
                        merge.extend(value)
                else:
                    raise ConstructorError(
                        "while constructing a mapping",
                        node.start_mark,
                        "expected a mapping or list of mappings for merging, "
                        "but found %s" % value_node.id,
                        value_node.start_mark,
                    )
            elif key_node.tag == u"tag:yaml.org,2002:value":
                key_node.tag = u"tag:yaml.org,2002:str"
                index += 1
            else:
                index += 1
        if bool(merge):
            node.merge = (
                merge
            )  # separate merge keys to be able to update without duplicate
            node.value = merge + node.value

    def construct_mapping(self, node, deep=False):
        # type: (Any, bool) -> Any
        """deep is True when creating an object/mapping recursively,
        in that case want the underlying elements available during construction
        """
        if isinstance(node, MappingNode):
            self.flatten_mapping(node)
        return BaseConstructor.construct_mapping(self, node, deep=deep)

    def construct_yaml_null(self, node):
        # type: (Any) -> Any
        self.construct_scalar(node)
        return None

    # YAML 1.2 spec doesn't mention yes/no etc any more, 1.1 does
    bool_values = {
        u"yes": True,
        u"no": False,
        u"y": True,
        u"n": False,
        u"true": True,
        u"false": False,
        u"on": True,
        u"off": False,
    }

    def construct_yaml_bool(self, node):
        # type: (Any) -> bool
        value = self.construct_scalar(node)
        return self.bool_values[value.lower()]

    def construct_yaml_int(self, node):
        # type: (Any) -> int
        value_s = to_str(self.construct_scalar(node))
        value_s = value_s.replace("_", "")
        sign = +1
        if value_s[0] == "-":
            sign = -1
        if value_s[0] in "+-":
            value_s = value_s[1:]
        if value_s == "0":
            return 0
        elif value_s.startswith("0b"):
            return sign * int(value_s[2:], 2)
        elif value_s.startswith("0x"):
            return sign * int(value_s[2:], 16)
        elif value_s.startswith("0o"):
            return sign * int(value_s[2:], 8)
        elif self.resolver.processing_version == (1, 1) and value_s[0] == "0":
            return sign * int(value_s, 8)
        elif self.resolver.processing_version == (1, 1) and ":" in value_s:
            digits = [int(part) for part in value_s.split(":")]
            digits.reverse()
            base = 1
            value = 0
            for digit in digits:
                value += digit * base
                base *= 60
            return sign * value
        else:
            return sign * int(value_s)

    inf_value = 1e300
    while inf_value != inf_value * inf_value:
        inf_value *= inf_value
    nan_value = -inf_value / inf_value  # Trying to make a quiet NaN (like C99).

    def construct_yaml_float(self, node):
        # type: (Any) -> float
        value_so = to_str(self.construct_scalar(node))
        value_s = value_so.replace("_", "").lower()
        sign = +1
        if value_s[0] == "-":
            sign = -1
        if value_s[0] in "+-":
            value_s = value_s[1:]
        if value_s == ".inf":
            return sign * self.inf_value
        elif value_s == ".nan":
            return self.nan_value
        elif self.resolver.processing_version != (1, 2) and ":" in value_s:
            digits = [float(part) for part in value_s.split(":")]
            digits.reverse()
            base = 1
            value = 0.0
            for digit in digits:
                value += digit * base
                base *= 60
            return sign * value
        else:
            if self.resolver.processing_version != (1, 2) and "e" in value_s:
                # value_s is lower case independent of input
                mantissa, exponent = value_s.split("e")
                if "." not in mantissa:
                    warnings.warn(MantissaNoDotYAML1_1Warning(node, value_so))
            return sign * float(value_s)

    if PY3:

        def construct_yaml_binary(self, node):
            # type: (Any) -> Any
            try:
                value = self.construct_scalar(node).encode("ascii")
            except UnicodeEncodeError as exc:
                raise ConstructorError(
                    None,
                    None,
                    "failed to convert base64 data into ascii: %s" % exc,
                    node.start_mark,
                )
            try:
                if hasattr(base64, "decodebytes"):
                    return base64.decodebytes(value)
                else:
                    return base64.decodestring(value)
            except binascii.Error as exc:
                raise ConstructorError(
                    None,
                    None,
                    "failed to decode base64 data: %s" % exc,
                    node.start_mark,
                )

    else:

        def construct_yaml_binary(self, node):
            # type: (Any) -> Any
            value = self.construct_scalar(node)
            try:
                return to_str(value).decode("base64")
            except (binascii.Error, UnicodeEncodeError) as exc:
                raise ConstructorError(
                    None,
                    None,
                    "failed to decode base64 data: %s" % exc,
                    node.start_mark,
                )

    timestamp_regexp = RegExp(
        u"""^(?P<year>[0-9][0-9][0-9][0-9])
          -(?P<month>[0-9][0-9]?)
          -(?P<day>[0-9][0-9]?)
          (?:((?P<t>[Tt])|[ \\t]+)   # explictly not retaining extra spaces
          (?P<hour>[0-9][0-9]?)
          :(?P<minute>[0-9][0-9])
          :(?P<second>[0-9][0-9])
          (?:\\.(?P<fraction>[0-9]*))?
          (?:[ \\t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
          (?::(?P<tz_minute>[0-9][0-9]))?))?)?$""",
        re.X,
    )

    def construct_yaml_timestamp(self, node, values=None):
        # type: (Any, Any) -> Any
        if values is None:
            try:
                match = self.timestamp_regexp.match(node.value)
            except TypeError:
                match = None
            if match is None:
                raise ConstructorError(
                    None,
                    None,
                    'failed to construct timestamp from "{}"'.format(node.value),
                    node.start_mark,
                )
            values = match.groupdict()
        year = int(values["year"])
        month = int(values["month"])
        day = int(values["day"])
        if not values["hour"]:
            return datetime.date(year, month, day)
        hour = int(values["hour"])
        minute = int(values["minute"])
        second = int(values["second"])
        fraction = 0
        if values["fraction"]:
            fraction_s = values["fraction"][:6]
            while len(fraction_s) < 6:
                fraction_s += "0"
            fraction = int(fraction_s)
            if len(values["fraction"]) > 6 and int(values["fraction"][6]) > 4:
                fraction += 1
        delta = None
        if values["tz_sign"]:
            tz_hour = int(values["tz_hour"])
            minutes = values["tz_minute"]
            tz_minute = int(minutes) if minutes else 0
            delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute)
            if values["tz_sign"] == "-":
                delta = -delta
        # should do something else instead (or hook this up to the preceding if statement
        # in reverse
        #  if delta is None:
        #      return datetime.datetime(year, month, day, hour, minute, second, fraction)
        #  return datetime.datetime(year, month, day, hour, minute, second, fraction,
        #                           datetime.timezone.utc)
        # the above is not good enough though, should provide tzinfo. In Python3 that is easily
        # doable drop that kind of support for Python2 as it has not native tzinfo
        data = datetime.datetime(year, month, day, hour, minute, second, fraction)
        if delta:
            data -= delta
        return data

    def construct_yaml_omap(self, node):
        # type: (Any) -> Any
        # Note: we do now check for duplicate keys
        omap = ordereddict()
        yield omap
        if not isinstance(node, SequenceNode):
            raise ConstructorError(
                "while constructing an ordered map",
                node.start_mark,
                "expected a sequence, but found %s" % node.id,
                node.start_mark,
            )
        for subnode in node.value:
            if not isinstance(subnode, MappingNode):
                raise ConstructorError(
                    "while constructing an ordered map",
                    node.start_mark,
                    "expected a mapping of length 1, but found %s" % subnode.id,
                    subnode.start_mark,
                )
            if len(subnode.value) != 1:
                raise ConstructorError(
                    "while constructing an ordered map",
                    node.start_mark,
                    "expected a single mapping item, but found %d items"
                    % len(subnode.value),
                    subnode.start_mark,
                )
            key_node, value_node = subnode.value[0]
            key = self.construct_object(key_node)
            assert key not in omap
            value = self.construct_object(value_node)
            omap[key] = value

    def construct_yaml_pairs(self, node):
        # type: (Any) -> Any
        # Note: the same code as `construct_yaml_omap`.
        pairs = []  # type: List[Any]
        yield pairs
        if not isinstance(node, SequenceNode):
            raise ConstructorError(
                "while constructing pairs",
                node.start_mark,
                "expected a sequence, but found %s" % node.id,
                node.start_mark,
            )
        for subnode in node.value:
            if not isinstance(subnode, MappingNode):
                raise ConstructorError(
                    "while constructing pairs",
                    node.start_mark,
                    "expected a mapping of length 1, but found %s" % subnode.id,
                    subnode.start_mark,
                )
            if len(subnode.value) != 1:
                raise ConstructorError(
                    "while constructing pairs",
                    node.start_mark,
                    "expected a single mapping item, but found %d items"
                    % len(subnode.value),
                    subnode.start_mark,
                )
            key_node, value_node = subnode.value[0]
            key = self.construct_object(key_node)
            value = self.construct_object(value_node)
            pairs.append((key, value))

    def construct_yaml_set(self, node):
        # type: (Any) -> Any
        data = set()  # type: Set[Any]
        yield data
        value = self.construct_mapping(node)
        data.update(value)

    def construct_yaml_str(self, node):
        # type: (Any) -> Any
        value = self.construct_scalar(node)
        if PY3:
            return value
        try:
            return value.encode("ascii")
        except UnicodeEncodeError:
            return value

    def construct_yaml_seq(self, node):
        # type: (Any) -> Any
        data = self.yaml_base_list_type()  # type: List[Any]
        yield data
        data.extend(self.construct_sequence(node))

    def construct_yaml_map(self, node):
        # type: (Any) -> Any
        data = self.yaml_base_dict_type()  # type: Dict[Any, Any]
        yield data
        value = self.construct_mapping(node)
        data.update(value)

    def construct_yaml_object(self, node, cls):
        # type: (Any, Any) -> Any
        data = cls.__new__(cls)
        yield data
        if hasattr(data, "__setstate__"):
            state = self.construct_mapping(node, deep=True)
            data.__setstate__(state)
        else:
            state = self.construct_mapping(node)
            data.__dict__.update(state)

    def construct_undefined(self, node):
        # type: (Any) -> None
        raise ConstructorError(
            None,
            None,
            "could not determine a constructor for the tag %r" % utf8(node.tag),
            node.start_mark,
        ) class Constructor(SafeConstructor):
    def construct_python_str(self, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_unicode(self, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    if PY3:

        def construct_python_bytes(self, node):
            raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_long(self, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_complex(self, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_tuple(self, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    def find_python_module(self, name, mark):
        raise ValueError("Unsafe constructor not implemented in this library")

    def find_python_name(self, name, mark):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_name(self, suffix, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_module(self, suffix, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False):
        raise ValueError("Unsafe constructor not implemented in this library")

    def set_python_instance_state(self, instance, state):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_object(self, suffix, node):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_object_apply(self, suffix, node, newobj=False):
        raise ValueError("Unsafe constructor not implemented in this library")

    def construct_python_object_new(self, suffix, node):
        raise ValueError("Unsafe constructor not implemented in this library") class RoundTripConstructor(SafeConstructor):
    """need to store the comments on the node itself,
    as well as on the items
    """

    def construct_scalar(self, node):
        # type: (Any) -> Any
        if not isinstance(node, ScalarNode):
            raise ConstructorError(
                None,
                None,
                "expected a scalar node, but found %s" % node.id,
                node.start_mark,
            )

        if node.style == "|" and isinstance(node.value, text_type):
            lss = LiteralScalarString(node.value, anchor=node.anchor)
            if node.comment and node.comment[1]:
                lss.comment = node.comment[1][0]  # type: ignore
            return lss
        if node.style == ">" and isinstance(node.value, text_type):
            fold_positions = []  # type: List[int]
            idx = -1
            while True:
                idx = node.value.find("\a", idx + 1)
                if idx < 0:
                    break
                fold_positions.append(idx - len(fold_positions))
            fss = FoldedScalarString(node.value.replace("\a", ""), anchor=node.anchor)
            if node.comment and node.comment[1]:
                fss.comment = node.comment[1][0]  # type: ignore
            if fold_positions:
                fss.fold_pos = fold_positions  # type: ignore
            return fss
        elif bool(self._preserve_quotes) and isinstance(node.value, text_type):
            if node.style == "'":
                return SingleQuotedScalarString(node.value, anchor=node.anchor)
            if node.style == '"':
                return DoubleQuotedScalarString(node.value, anchor=node.anchor)
        if node.anchor:
            return PlainScalarString(node.value, anchor=node.anchor)
        return node.value

    def construct_yaml_int(self, node):
        # type: (Any) -> Any
        width = None  # type: Any
        value_su = to_str(self.construct_scalar(node))
        try:
            sx = value_su.rstrip("_")
            underscore = [len(sx) - sx.rindex("_") - 1, False, False]  # type: Any
        except ValueError:
            underscore = None
        except IndexError:
            underscore = None
        value_s = value_su.replace("_", "")
        sign = +1
        if value_s[0] == "-":
            sign = -1
        if value_s[0] in "+-":
            value_s = value_s[1:]
        if value_s == "0":
            return 0
        elif value_s.startswith("0b"):
            if self.resolver.processing_version > (1, 1) and value_s[2] == "0":
                width = len(value_s[2:])
            if underscore is not None:
                underscore[1] = value_su[2] == "_"
                underscore[2] = len(value_su[2:]) > 1 and value_su[-1] == "_"
            return BinaryInt(
                sign * int(value_s[2:], 2),
                width=width,
                underscore=underscore,
                anchor=node.anchor,
            )
        elif value_s.startswith("0x"):
            # default to lower-case if no a-fA-F in string
            if self.resolver.processing_version > (1, 1) and value_s[2] == "0":
                width = len(value_s[2:])
            hex_fun = HexInt  # type: Any
            for ch in value_s[2:]:
                if ch in "ABCDEF":  # first non-digit is capital
                    hex_fun = HexCapsInt
                    break
                if ch in "abcdef":
                    break
            if underscore is not None:
                underscore[1] = value_su[2] == "_"
                underscore[2] = len(value_su[2:]) > 1 and value_su[-1] == "_"
            return hex_fun(
                sign * int(value_s[2:], 16),
                width=width,
                underscore=underscore,
                anchor=node.anchor,
            )
        elif value_s.startswith("0o"):
            if self.resolver.processing_version > (1, 1) and value_s[2] == "0":
                width = len(value_s[2:])
            if underscore is not None:
                underscore[1] = value_su[2] == "_"
                underscore[2] = len(value_su[2:]) > 1 and value_su[-1] == "_"
            return OctalInt(
                sign * int(value_s[2:], 8),
                width=width,
                underscore=underscore,
                anchor=node.anchor,
            )
        elif self.resolver.processing_version != (1, 2) and value_s[0] == "0":
            return sign * int(value_s, 8)
        elif self.resolver.processing_version != (1, 2) and ":" in value_s:
            digits = [int(part) for part in value_s.split(":")]
            digits.reverse()
            base = 1
            value = 0
            for digit in digits:
                value += digit * base
                base *= 60
            return sign * value
        elif self.resolver.processing_version > (1, 1) and value_s[0] == "0":
            # not an octal, an integer with leading zero(s)
            if underscore is not None:
                # cannot have a leading underscore
                underscore[2] = len(value_su) > 1 and value_su[-1] == "_"
            return ScalarInt(
                sign * int(value_s), width=len(value_s), underscore=underscore
            )
        elif underscore:
            # cannot have a leading underscore
            underscore[2] = len(value_su) > 1 and value_su[-1] == "_"
            return ScalarInt(
                sign * int(value_s),
                width=None,
                underscore=underscore,
                anchor=node.anchor,
            )
        elif node.anchor:
            return ScalarInt(sign * int(value_s), width=None, anchor=node.anchor)
        else:
            return sign * int(value_s)

    def construct_yaml_float(self, node):
        # type: (Any) -> Any
        def leading_zeros(v):
            # type: (Any) -> int
            lead0 = 0
            idx = 0
            while idx < len(v) and v[idx] in "0.":
                if v[idx] == "0":
                    lead0 += 1
                idx += 1
            return lead0

        # underscore = None
        m_sign = False  # type: Any
        value_so = to_str(self.construct_scalar(node))
        value_s = value_so.replace("_", "").lower()
        sign = +1
        if value_s[0] == "-":
            sign = -1
        if value_s[0] in "+-":
            m_sign = value_s[0]
            value_s = value_s[1:]
        if value_s == ".inf":
            return sign * self.inf_value
        if value_s == ".nan":
            return self.nan_value
        if self.resolver.processing_version != (1, 2) and ":" in value_s:
            digits = [float(part) for part in value_s.split(":")]
            digits.reverse()
            base = 1
            value = 0.0
            for digit in digits:
                value += digit * base
                base *= 60
            return sign * value
        if "e" in value_s:
            try:
                mantissa, exponent = value_so.split("e")
                exp = "e"
            except ValueError:
                mantissa, exponent = value_so.split("E")
                exp = "E"
            if self.resolver.processing_version != (1, 2):
                # value_s is lower case independent of input
                if "." not in mantissa:
                    warnings.warn(MantissaNoDotYAML1_1Warning(node, value_so))
            lead0 = leading_zeros(mantissa)
            width = len(mantissa)
            prec = mantissa.find(".")
            if m_sign:
                width -= 1
            e_width = len(exponent)
            e_sign = exponent[0] in "+-"
            # nprint('sf', width, prec, m_sign, exp, e_width, e_sign)
            return ScalarFloat(
                sign * float(value_s),
                width=width,
                prec=prec,
                m_sign=m_sign,
                m_lead0=lead0,
                exp=exp,
                e_width=e_width,
                e_sign=e_sign,
                anchor=node.anchor,
            )
        width = len(value_so)
        prec = value_so.index(
            "."
        )  # you can use index, this would not be float without dot
        lead0 = leading_zeros(value_so)
        return ScalarFloat(
            sign * float(value_s),
            width=width,
            prec=prec,
            m_sign=m_sign,
            m_lead0=lead0,
            anchor=node.anchor,
        )

    def construct_yaml_str(self, node):
        # type: (Any) -> Any
        value = self.construct_scalar(node)
        if isinstance(value, ScalarString):
            return value
        if PY3:
            return value
        try:
            return value.encode("ascii")
        except AttributeError:
            # in case you replace the node dynamically e.g. with a dict
            return value
        except UnicodeEncodeError:
            return value

    def construct_rt_sequence(self, node, seqtyp, deep=False):
        # type: (Any, Any, bool) -> Any
        if not isinstance(node, SequenceNode):
            raise ConstructorError(
                None,
                None,
                "expected a sequence node, but found %s" % node.id,
                node.start_mark,
            )
        ret_val = []
        if node.comment:
            seqtyp._yaml_add_comment(node.comment[:2])
            if len(node.comment) > 2:
                seqtyp.yaml_end_comment_extend(node.comment[2], clear=True)
        if node.anchor:
            from .serializer import templated_id

            if not templated_id(node.anchor):
                seqtyp.yaml_set_anchor(node.anchor)
        for idx, child in enumerate(node.value):
            if child.comment:
                seqtyp._yaml_add_comment(child.comment, key=idx)
                child.comment = None  # if moved to sequence remove from child
            ret_val.append(self.construct_object(child, deep=deep))
            seqtyp._yaml_set_idx_line_col(
                idx, [child.start_mark.line, child.start_mark.column]
            )
        return ret_val

    def flatten_mapping(self, node):
        # type: (Any) -> Any
        """
        This implements the merge key feature http://yaml.org/type/merge.html
        by inserting keys from the merge dict/list of dicts if not yet
        available in this node
        """

        def constructed(value_node):
            # type: (Any) -> Any
            # If the contents of a merge are defined within the
            # merge marker, then they won't have been constructed
            # yet. But if they were already constructed, we need to use
            # the existing object.
            if value_node in self.constructed_objects:
                value = self.constructed_objects[value_node]
            else:
                value = self.construct_object(value_node, deep=False)
            return value

        # merge = []
        merge_map_list = []  # type: List[Any]
        index = 0
        while index < len(node.value):
            key_node, value_node = node.value[index]
            if key_node.tag == u"tag:yaml.org,2002:merge":
                if merge_map_list:  # double << key
                    if self.allow_duplicate_keys:
                        del node.value[index]
                        index += 1
                        continue
                    args = [
                        "while constructing a mapping",
                        node.start_mark,
                        'found duplicate key "{}"'.format(key_node.value),
                        key_node.start_mark,
                        """
                        To suppress this check see:
                           http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys
                        """,
                        """\
                        Duplicate keys will become an error in future releases, and are errors
                        by default when using the new API.
                        """,
                    ]
                    if self.allow_duplicate_keys is None:
                        warnings.warn(DuplicateKeyFutureWarning(*args))
                    else:
                        raise DuplicateKeyError(*args)
                del node.value[index]
                if isinstance(value_node, MappingNode):
                    merge_map_list.append((index, constructed(value_node)))
                    # self.flatten_mapping(value_node)
                    # merge.extend(value_node.value)
                elif isinstance(value_node, SequenceNode):
                    # submerge = []
                    for subnode in value_node.value:
                        if not isinstance(subnode, MappingNode):
                            raise ConstructorError(
                                "while constructing a mapping",
                                node.start_mark,
                                "expected a mapping for merging, but found %s"
                                % subnode.id,
                                subnode.start_mark,
                            )
                        merge_map_list.append((index, constructed(subnode)))
                    #     self.flatten_mapping(subnode)
                    #     submerge.append(subnode.value)
                    # submerge.reverse()
                    # for value in submerge:
                    #     merge.extend(value)
                else:
                    raise ConstructorError(
                        "while constructing a mapping",
                        node.start_mark,
                        "expected a mapping or list of mappings for merging, "
                        "but found %s" % value_node.id,
                        value_node.start_mark,
                    )
            elif key_node.tag == u"tag:yaml.org,2002:value":
                key_node.tag = u"tag:yaml.org,2002:str"
                index += 1
            else:
                index += 1
        return merge_map_list
        # if merge:
        #     node.value = merge + node.value

    def _sentinel(self):
        # type: () -> None
        pass

    def construct_mapping(self, node, maptyp, deep=False):  # type: ignore
        # type: (Any, Any, bool) -> Any
        if not isinstance(node, MappingNode):
            raise ConstructorError(
                None,
                None,
                "expected a mapping node, but found %s" % node.id,
                node.start_mark,
            )
        merge_map = self.flatten_mapping(node)
        # mapping = {}
        if node.comment:
            maptyp._yaml_add_comment(node.comment[:2])
            if len(node.comment) > 2:
                maptyp.yaml_end_comment_extend(node.comment[2], clear=True)
        if node.anchor:
            from .serializer import templated_id

            if not templated_id(node.anchor):
                maptyp.yaml_set_anchor(node.anchor)
        last_key, last_value = None, self._sentinel
        for key_node, value_node in node.value:
            # keys can be list -> deep
            key = self.construct_object(key_node, deep=True)
            # lists are not hashable, but tuples are
            if not isinstance(key, Hashable):
                if isinstance(key, MutableSequence):
                    key_s = CommentedKeySeq(key)
                    if key_node.flow_style is True:
                        key_s.fa.set_flow_style()
                    elif key_node.flow_style is False:
                        key_s.fa.set_block_style()
                    key = key_s
                elif isinstance(key, MutableMapping):
                    key_m = CommentedKeyMap(key)
                    if key_node.flow_style is True:
                        key_m.fa.set_flow_style()
                    elif key_node.flow_style is False:
                        key_m.fa.set_block_style()
                    key = key_m
            if PY2:
                try:
                    hash(key)
                except TypeError as exc:
                    raise ConstructorError(
                        "while constructing a mapping",
                        node.start_mark,
                        "found unacceptable key (%s)" % exc,
                        key_node.start_mark,
                    )
            else:
                if not isinstance(key, Hashable):
                    raise ConstructorError(
                        "while constructing a mapping",
                        node.start_mark,
                        "found unhashable key",
                        key_node.start_mark,
                    )
            value = self.construct_object(value_node, deep=deep)
            if self.check_mapping_key(node, key_node, maptyp, key, value):
                if (
                    key_node.comment
                    and len(key_node.comment) > 4
                    and key_node.comment[4]
                ):
                    if last_value is None:
                        key_node.comment[0] = key_node.comment.pop(4)
                        maptyp._yaml_add_comment(key_node.comment, value=last_key)
                    else:
                        key_node.comment[2] = key_node.comment.pop(4)
                        maptyp._yaml_add_comment(key_node.comment, key=key)
                    key_node.comment = None
                if key_node.comment:
                    maptyp._yaml_add_comment(key_node.comment, key=key)
                if value_node.comment:
                    maptyp._yaml_add_comment(value_node.comment, value=key)
                maptyp._yaml_set_kv_line_col(
                    key,
                    [
                        key_node.start_mark.line,
                        key_node.start_mark.column,
                        value_node.start_mark.line,
                        value_node.start_mark.column,
                    ],
                )
                maptyp[key] = value
                last_key, last_value = key, value  # could use indexing
        # do this last, or <<: before a key will prevent insertion in instances
        # of collections.OrderedDict (as they have no __contains__
        if merge_map:
            maptyp.add_yaml_merge(merge_map)

    def construct_setting(self, node, typ, deep=False):
        # type: (Any, Any, bool) -> Any
        if not isinstance(node, MappingNode):
            raise ConstructorError(
                None,
                None,
                "expected a mapping node, but found %s" % node.id,
                node.start_mark,
            )
        if node.comment:
            typ._yaml_add_comment(node.comment[:2])
            if len(node.comment) > 2:
                typ.yaml_end_comment_extend(node.comment[2], clear=True)
        if node.anchor:
            from .serializer import templated_id

            if not templated_id(node.anchor):
                typ.yaml_set_anchor(node.anchor)
        for key_node, value_node in node.value:
            # keys can be list -> deep
            key = self.construct_object(key_node, deep=True)
            # lists are not hashable, but tuples are
            if not isinstance(key, Hashable):
                if isinstance(key, list):
                    key = tuple(key)
            if PY2:
                try:
                    hash(key)
                except TypeError as exc:
                    raise ConstructorError(
                        "while constructing a mapping",
                        node.start_mark,
                        "found unacceptable key (%s)" % exc,
                        key_node.start_mark,
                    )
            else:
                if not isinstance(key, Hashable):
                    raise ConstructorError(
                        "while constructing a mapping",
                        node.start_mark,
                        "found unhashable key",
                        key_node.start_mark,
                    )
            # construct but should be null
            value = self.construct_object(value_node, deep=deep)  # NOQA
            self.check_set_key(node, key_node, typ, key)
            if key_node.comment:
                typ._yaml_add_comment(key_node.comment, key=key)
            if value_node.comment:
                typ._yaml_add_comment(value_node.comment, value=key)
            typ.add(key)

    def construct_yaml_seq(self, node):
        # type: (Any) -> Any
        data = CommentedSeq()
        data._yaml_set_line_col(node.start_mark.line, node.start_mark.column)
        if node.comment:
            data._yaml_add_comment(node.comment)
        yield data
        data.extend(self.construct_rt_sequence(node, data))
        self.set_collection_style(data, node)

    def construct_yaml_map(self, node):
        # type: (Any) -> Any
        data = CommentedMap()
        data._yaml_set_line_col(node.start_mark.line, node.start_mark.column)
        yield data
        self.construct_mapping(node, data, deep=True)
        self.set_collection_style(data, node)

    def set_collection_style(self, data, node):
        # type: (Any, Any) -> None
        if len(data) == 0:
            return
        if node.flow_style is True:
            data.fa.set_flow_style()
        elif node.flow_style is False:
            data.fa.set_block_style()

    def construct_yaml_object(self, node, cls):
        # type: (Any, Any) -> Any
        data = cls.__new__(cls)
        yield data
        if hasattr(data, "__setstate__"):
            state = SafeConstructor.construct_mapping(self, node, deep=True)
            data.__setstate__(state)
        else:
            state = SafeConstructor.construct_mapping(self, node)
            data.__dict__.update(state)

    def construct_yaml_omap(self, node):
        # type: (Any) -> Any
        # Note: we do now check for duplicate keys
        omap = CommentedOrderedMap()
        omap._yaml_set_line_col(node.start_mark.line, node.start_mark.column)
        if node.flow_style is True:
            omap.fa.set_flow_style()
        elif node.flow_style is False:
            omap.fa.set_block_style()
        yield omap
        if node.comment:
            omap._yaml_add_comment(node.comment[:2])
            if len(node.comment) > 2:
                omap.yaml_end_comment_extend(node.comment[2], clear=True)
        if not isinstance(node, SequenceNode):
            raise ConstructorError(
                "while constructing an ordered map",
                node.start_mark,
                "expected a sequence, but found %s" % node.id,
                node.start_mark,
            )
        for subnode in node.value:
            if not isinstance(subnode, MappingNode):
                raise ConstructorError(
                    "while constructing an ordered map",
                    node.start_mark,
                    "expected a mapping of length 1, but found %s" % subnode.id,
                    subnode.start_mark,
                )
            if len(subnode.value) != 1:
                raise ConstructorError(
                    "while constructing an ordered map",
                    node.start_mark,
                    "expected a single mapping item, but found %d items"
                    % len(subnode.value),
                    subnode.start_mark,
                )
            key_node, value_node = subnode.value[0]
            key = self.construct_object(key_node)
            assert key not in omap
            value = self.construct_object(value_node)
            if key_node.comment:
                omap._yaml_add_comment(key_node.comment, key=key)
            if subnode.comment:
                omap._yaml_add_comment(subnode.comment, key=key)
            if value_node.comment:
                omap._yaml_add_comment(value_node.comment, value=key)
            omap[key] = value

    def construct_yaml_set(self, node):
        # type: (Any) -> Any
        data = CommentedSet()
        data._yaml_set_line_col(node.start_mark.line, node.start_mark.column)
        yield data
        self.construct_setting(node, data)

    def construct_undefined(self, node):
        # type: (Any) -> Any
        try:
            if isinstance(node, MappingNode):
                data = CommentedMap()
                data._yaml_set_line_col(node.start_mark.line, node.start_mark.column)
                if node.flow_style is True:
                    data.fa.set_flow_style()
                elif node.flow_style is False:
                    data.fa.set_block_style()
                data.yaml_set_tag(node.tag)
                yield data
                if node.anchor:
                    data.yaml_set_anchor(node.anchor)
                self.construct_mapping(node, data)
                return
            elif isinstance(node, ScalarNode):
                data2 = TaggedScalar()
                data2.value = self.construct_scalar(node)
                data2.style = node.style
                data2.yaml_set_tag(node.tag)
                yield data2
                if node.anchor:
                    data2.yaml_set_anchor(node.anchor, always_dump=True)
                return
            elif isinstance(node, SequenceNode):
                data3 = CommentedSeq()
                data3._yaml_set_line_col(node.start_mark.line, node.start_mark.column)
                if node.flow_style is True:
                    data3.fa.set_flow_style()
                elif node.flow_style is False:
                    data3.fa.set_block_style()
                data3.yaml_set_tag(node.tag)
                yield data3
                if node.anchor:
                    data3.yaml_set_anchor(node.anchor)
                data3.extend(self.construct_sequence(node))
                return
        except:  # NOQA
            pass
        raise ConstructorError(
            None,
            None,
            "could not determine a constructor for the tag %r" % utf8(node.tag),
            node.start_mark,
        )

    def construct_yaml_timestamp(self, node, values=None):
        # type: (Any, Any) -> Any
        try:
            match = self.timestamp_regexp.match(node.value)
        except TypeError:
            match = None
        if match is None:
            raise ConstructorError(
                None,
                None,
                'failed to construct timestamp from "{}"'.format(node.value),
                node.start_mark,
            )
        values = match.groupdict()
        if not values["hour"]:
            return SafeConstructor.construct_yaml_timestamp(self, node, values)
        for part in ["t", "tz_sign", "tz_hour", "tz_minute"]:
            if values[part]:
                break
        else:
            return SafeConstructor.construct_yaml_timestamp(self, node, values)
        year = int(values["year"])
        month = int(values["month"])
        day = int(values["day"])
        hour = int(values["hour"])
        minute = int(values["minute"])
        second = int(values["second"])
        fraction = 0
        if values["fraction"]:
            fraction_s = values["fraction"][:6]
            while len(fraction_s) < 6:
                fraction_s += "0"
            fraction = int(fraction_s)
            if len(values["fraction"]) > 6 and int(values["fraction"][6]) > 4:
                fraction += 1
        delta = None
        if values["tz_sign"]:
            tz_hour = int(values["tz_hour"])
            minutes = values["tz_minute"]
            tz_minute = int(minutes) if minutes else 0
            delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute)
            if values["tz_sign"] == "-":
                delta = -delta
        if delta:
            dt = datetime.datetime(year, month, day, hour, minute)
            dt -= delta
            data = TimeStamp(
                dt.year, dt.month, dt.day, dt.hour, dt.minute, second, fraction
            )
            data._yaml["delta"] = delta
            tz = values["tz_sign"] + values["tz_hour"]
            if values["tz_minute"]:
                tz += ":" + values["tz_minute"]
            data._yaml["tz"] = tz
        else:
            data = TimeStamp(year, month, day, hour, minute, second, fraction)
            if values["tz"]:  # no delta
                data._yaml["tz"] = values["tz"]

        if values["t"]:
            data._yaml["t"] = True
        return data

    def construct_yaml_bool(self, node):
        # type: (Any) -> Any
        b = SafeConstructor.construct_yaml_bool(self, node)
        if node.anchor:
            return ScalarBoolean(b, anchor=node.anchor)
        return b class VersionedResolver(BaseResolver):
    """
    contrary to the "normal" resolver, the smart resolver delays loading
    the pattern matching rules. That way it can decide to load 1.1 rules
    or the (default) 1.2 rules, that no longer support octal without 0o, sexagesimals
    and Yes/No/On/Off booleans.
    """

    def __init__(self, version=None, loader=None, loadumper=None):
        # type: (Optional[VersionType], Any, Any) -> None
        if loader is None and loadumper is not None:
            loader = loadumper
        BaseResolver.__init__(self, loader)
        self._loader_version = self.get_loader_version(version)
        self._version_implicit_resolver = {}  # type: Dict[Any, Any]

    def add_version_implicit_resolver(self, version, tag, regexp, first):
        # type: (VersionType, Any, Any, Any) -> None
        if first is None:
            first = [None]
        impl_resolver = self._version_implicit_resolver.setdefault(version, {})
        for ch in first:
            impl_resolver.setdefault(ch, []).append((tag, regexp))

    def get_loader_version(self, version):
        # type: (Optional[VersionType]) -> Any
        if version is None or isinstance(version, tuple):
            return version
        if isinstance(version, list):
            return tuple(version)
        # assume string
        return tuple(map(int, version.split(u".")))

    @property
    def versioned_resolver(self):
        # type: () -> Any
        """
        select the resolver based on the version we are parsing
        """
        version = self.processing_version
        if version not in self._version_implicit_resolver:
            for x in implicit_resolvers:
                if version in x[0]:
                    self.add_version_implicit_resolver(version, x[1], x[2], x[3])
        return self._version_implicit_resolver[version]

    def resolve(self, kind, value, implicit):
        # type: (Any, Any, Any) -> Any
        if kind is ScalarNode and implicit[0]:
            if value == "":
                resolvers = self.versioned_resolver.get("", [])
            else:
                resolvers = self.versioned_resolver.get(value[0], [])
            resolvers += self.versioned_resolver.get(None, [])
            for tag, regexp in resolvers:
                if regexp.match(value):
                    return tag
            implicit = implicit[1]
        if bool(self.yaml_path_resolvers):
            exact_paths = self.resolver_exact_paths[-1]
            if kind in exact_paths:
                return exact_paths[kind]
            if None in exact_paths:
                return exact_paths[None]
        if kind is ScalarNode:
            return self.DEFAULT_SCALAR_TAG
        elif kind is SequenceNode:
            return self.DEFAULT_SEQUENCE_TAG
        elif kind is MappingNode:
            return self.DEFAULT_MAPPING_TAG

    @property
    def processing_version(self):
        # type: () -> Any
        try:
            version = self.loadumper._scanner.yaml_version
        except AttributeError:
            try:
                if hasattr(self.loadumper, "typ"):
                    version = self.loadumper.version
                else:
                    version = self.loadumper._serializer.use_version  # dumping
            except AttributeError:
                version = None
        if version is None:
            version = self._loader_version
            if version is None:
                version = _DEFAULT_YAML_VERSION
        return version
from .reader import Reader
from .scanner import Scanner, RoundTripScanner
from .parser import Parser, RoundTripParser
from .composer import Composer
from .constructor import (
    BaseConstructor,
    SafeConstructor,
    Constructor,
    RoundTripConstructor,
)
from .resolver import VersionedResolver
    from typing import Any, Dict, List, Union, Optional  # NOQA
    from .compat import StreamTextType, VersionType  # NOQA
# coding: utf-8

from __future__ import absolute_import



if False:  # MYPY

__all__ = ["BaseLoader", "SafeLoader", "Loader", "RoundTripLoader"]


class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, VersionedResolver):
    def __init__(self, stream, version=None, preserve_quotes=None):
        # type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
        Reader.__init__(self, stream, loader=self)
        Scanner.__init__(self, loader=self)
        Parser.__init__(self, loader=self)
        Composer.__init__(self, loader=self)
        BaseConstructor.__init__(self, loader=self)
        VersionedResolver.__init__(self, version, loader=self)



class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, VersionedResolver):