~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/knit.py

(jelmer) Use the absolute_import feature everywhere in bzrlib,
 and add a source test to make sure it's used everywhere. (Jelmer Vernooij)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2008 Canonical Ltd
 
1
# Copyright (C) 2006-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
51
51
 
52
52
"""
53
53
 
 
54
from __future__ import absolute_import
 
55
 
54
56
 
55
57
from cStringIO import StringIO
56
58
from itertools import izip
57
59
import operator
58
60
import os
59
 
import sys
60
61
 
61
62
from bzrlib.lazy_import import lazy_import
62
63
lazy_import(globals(), """
 
64
import gzip
 
65
 
63
66
from bzrlib import (
64
 
    annotate,
65
67
    debug,
66
68
    diff,
67
69
    graph as _mod_graph,
68
70
    index as _mod_index,
69
 
    lru_cache,
70
71
    pack,
71
 
    progress,
 
72
    patiencediff,
72
73
    static_tuple,
73
74
    trace,
74
75
    tsort,
75
76
    tuned_gzip,
 
77
    ui,
76
78
    )
 
79
 
 
80
from bzrlib.repofmt import pack_repo
 
81
from bzrlib.i18n import gettext
77
82
""")
78
83
from bzrlib import (
 
84
    annotate,
79
85
    errors,
80
86
    osutils,
81
 
    patiencediff,
82
87
    )
83
88
from bzrlib.errors import (
84
 
    FileExists,
85
89
    NoSuchFile,
86
 
    KnitError,
87
90
    InvalidRevisionId,
88
91
    KnitCorrupt,
89
92
    KnitHeaderError,
90
93
    RevisionNotPresent,
91
 
    RevisionAlreadyPresent,
92
94
    SHA1KnitCorrupt,
93
95
    )
94
96
from bzrlib.osutils import (
95
97
    contains_whitespace,
96
 
    contains_linebreaks,
97
98
    sha_string,
98
99
    sha_strings,
99
100
    split_lines,
100
101
    )
101
102
from bzrlib.versionedfile import (
 
103
    _KeyRefs,
102
104
    AbsentContentFactory,
103
105
    adapter_registry,
104
106
    ConstantMapper,
105
107
    ContentFactory,
106
 
    ChunkedContentFactory,
107
108
    sort_groupcompress,
108
 
    VersionedFile,
109
 
    VersionedFiles,
 
109
    VersionedFilesWithFallbacks,
110
110
    )
111
111
 
112
112
 
411
411
class KnitContent(object):
412
412
    """Content of a knit version to which deltas can be applied.
413
413
 
414
 
    This is always stored in memory as a list of lines with \n at the end,
 
414
    This is always stored in memory as a list of lines with \\n at the end,
415
415
    plus a flag saying if the final ending is really there or not, because that
416
416
    corresponds to the on-disk knit representation.
417
417
    """
804
804
        writer.begin()
805
805
        index = _KnitGraphIndex(graph_index, lambda:True, parents=parents,
806
806
            deltas=delta, add_callback=graph_index.add_nodes)
807
 
        access = _DirectPackAccess({})
 
807
        access = pack_repo._DirectPackAccess({})
808
808
        access.set_writer(writer, graph_index, (transport, 'newpack'))
809
809
        result = KnitVersionedFiles(index, access,
810
810
            max_delta_chain=max_delta_chain)
848
848
                in all_build_index_memos.itervalues()])
849
849
 
850
850
 
851
 
class KnitVersionedFiles(VersionedFiles):
 
851
class KnitVersionedFiles(VersionedFilesWithFallbacks):
852
852
    """Storage for many versioned files using knit compression.
853
853
 
854
854
    Backend storage is managed by indices and data objects.
881
881
            self._factory = KnitAnnotateFactory()
882
882
        else:
883
883
            self._factory = KnitPlainFactory()
884
 
        self._fallback_vfs = []
 
884
        self._immediate_fallback_vfs = []
885
885
        self._reload_func = reload_func
886
886
 
887
887
    def __repr__(self):
890
890
            self._index,
891
891
            self._access)
892
892
 
 
893
    def without_fallbacks(self):
 
894
        """Return a clone of this object without any fallbacks configured."""
 
895
        return KnitVersionedFiles(self._index, self._access,
 
896
            self._max_delta_chain, self._factory.annotated,
 
897
            self._reload_func)
 
898
 
893
899
    def add_fallback_versioned_files(self, a_versioned_files):
894
900
        """Add a source of texts for texts not present in this knit.
895
901
 
896
902
        :param a_versioned_files: A VersionedFiles object.
897
903
        """
898
 
        self._fallback_vfs.append(a_versioned_files)
 
904
        self._immediate_fallback_vfs.append(a_versioned_files)
899
905
 
900
906
    def add_lines(self, key, parents, lines, parent_texts=None,
901
907
        left_matching_blocks=None, nostore_sha=None, random_id=False,
1068
1074
                    raise errors.KnitCorrupt(self,
1069
1075
                        "Missing basis parent %s for %s" % (
1070
1076
                        compression_parent, key))
1071
 
        for fallback_vfs in self._fallback_vfs:
 
1077
        for fallback_vfs in self._immediate_fallback_vfs:
1072
1078
            fallback_vfs.check()
1073
1079
 
1074
1080
    def _check_add(self, key, lines, random_id, check_content):
1152
1158
 
1153
1159
        A dict of key to (record_details, index_memo, next, parents) is
1154
1160
        returned.
1155
 
        method is the way referenced data should be applied.
1156
 
        index_memo is the handle to pass to the data access to actually get the
1157
 
            data
1158
 
        next is the build-parent of the version, or None for fulltexts.
1159
 
        parents is the version_ids of the parents of this version
1160
 
 
1161
 
        :param allow_missing: If True do not raise an error on a missing component,
1162
 
            just ignore it.
 
1161
 
 
1162
        * method is the way referenced data should be applied.
 
1163
        * index_memo is the handle to pass to the data access to actually get
 
1164
          the data
 
1165
        * next is the build-parent of the version, or None for fulltexts.
 
1166
        * parents is the version_ids of the parents of this version
 
1167
 
 
1168
        :param allow_missing: If True do not raise an error on a missing
 
1169
            component, just ignore it.
1163
1170
        """
1164
1171
        component_data = {}
1165
1172
        pending_components = keys
1191
1198
        generator = _VFContentMapGenerator(self, [key])
1192
1199
        return generator._get_content(key)
1193
1200
 
1194
 
    def get_known_graph_ancestry(self, keys):
1195
 
        """Get a KnownGraph instance with the ancestry of keys."""
1196
 
        parent_map, missing_keys = self._index.find_ancestry(keys)
1197
 
        for fallback in self._fallback_vfs:
1198
 
            if not missing_keys:
1199
 
                break
1200
 
            (f_parent_map, f_missing_keys) = fallback._index.find_ancestry(
1201
 
                                                missing_keys)
1202
 
            parent_map.update(f_parent_map)
1203
 
            missing_keys = f_missing_keys
1204
 
        kg = _mod_graph.KnownGraph(parent_map)
1205
 
        return kg
1206
 
 
1207
1201
    def get_parent_map(self, keys):
1208
1202
        """Get a map of the graph parents of keys.
1209
1203
 
1224
1218
            and so on.
1225
1219
        """
1226
1220
        result = {}
1227
 
        sources = [self._index] + self._fallback_vfs
 
1221
        sources = [self._index] + self._immediate_fallback_vfs
1228
1222
        source_results = []
1229
1223
        missing = set(keys)
1230
1224
        for source in sources:
1240
1234
        """Produce a dictionary of knit records.
1241
1235
 
1242
1236
        :return: {key:(record, record_details, digest, next)}
1243
 
            record
1244
 
                data returned from read_records (a KnitContentobject)
1245
 
            record_details
1246
 
                opaque information to pass to parse_record
1247
 
            digest
1248
 
                SHA1 digest of the full text after all steps are done
1249
 
            next
1250
 
                build-parent of the version, i.e. the leftmost ancestor.
 
1237
 
 
1238
            * record: data returned from read_records (a KnitContentobject)
 
1239
            * record_details: opaque information to pass to parse_record
 
1240
            * digest: SHA1 digest of the full text after all steps are done
 
1241
            * next: build-parent of the version, i.e. the leftmost ancestor.
1251
1242
                Will be None if the record is not a delta.
 
1243
 
1252
1244
        :param keys: The keys to build a map for
1253
1245
        :param allow_missing: If some records are missing, rather than
1254
1246
            error, just return the data that could be generated.
1524
1516
                        yield KnitContentFactory(key, global_map[key],
1525
1517
                            record_details, None, raw_data, self._factory.annotated, None)
1526
1518
                else:
1527
 
                    vf = self._fallback_vfs[parent_maps.index(source) - 1]
 
1519
                    vf = self._immediate_fallback_vfs[parent_maps.index(source) - 1]
1528
1520
                    for record in vf.get_record_stream(keys, ordering,
1529
1521
                        include_delta_closure):
1530
1522
                        yield record
1540
1532
            # record entry 2 is the 'digest'.
1541
1533
            result[key] = details[2]
1542
1534
        missing.difference_update(set(result))
1543
 
        for source in self._fallback_vfs:
 
1535
        for source in self._immediate_fallback_vfs:
1544
1536
            if not missing:
1545
1537
                break
1546
1538
            new_result = source.get_sha1s(missing)
1617
1609
                raise RevisionNotPresent([record.key], self)
1618
1610
            elif ((record.storage_kind in knit_types)
1619
1611
                  and (compression_parent is None
1620
 
                       or not self._fallback_vfs
 
1612
                       or not self._immediate_fallback_vfs
1621
1613
                       or self._index.has_key(compression_parent)
1622
1614
                       or not self.has_key(compression_parent))):
1623
1615
                # we can insert the knit record literally if either it has no
1755
1747
        :return: An iterator over (line, key).
1756
1748
        """
1757
1749
        if pb is None:
1758
 
            pb = progress.DummyProgress()
 
1750
            pb = ui.ui_factory.nested_progress_bar()
1759
1751
        keys = set(keys)
1760
1752
        total = len(keys)
1761
1753
        done = False
1771
1763
                        key_records.append((key, details[0]))
1772
1764
                records_iter = enumerate(self._read_records_iter(key_records))
1773
1765
                for (key_idx, (key, data, sha_value)) in records_iter:
1774
 
                    pb.update('Walking content', key_idx, total)
 
1766
                    pb.update(gettext('Walking content'), key_idx, total)
1775
1767
                    compression_parent = build_details[key][1]
1776
1768
                    if compression_parent is None:
1777
1769
                        # fulltext
1795
1787
        # vfs, and hope to find them there.  Note that if the keys are found
1796
1788
        # but had no changes or no content, the fallback may not return
1797
1789
        # anything.
1798
 
        if keys and not self._fallback_vfs:
 
1790
        if keys and not self._immediate_fallback_vfs:
1799
1791
            # XXX: strictly the second parameter is meant to be the file id
1800
1792
            # but it's not easily accessible here.
1801
1793
            raise RevisionNotPresent(keys, repr(self))
1802
 
        for source in self._fallback_vfs:
 
1794
        for source in self._immediate_fallback_vfs:
1803
1795
            if not keys:
1804
1796
                break
1805
1797
            source_keys = set()
1807
1799
                source_keys.add(key)
1808
1800
                yield line, key
1809
1801
            keys.difference_update(source_keys)
1810
 
        pb.update('Walking content', total, total)
 
1802
        pb.update(gettext('Walking content'), total, total)
1811
1803
 
1812
1804
    def _make_line_delta(self, delta_seq, new_content):
1813
1805
        """Generate a line delta from delta_seq and new_content."""
1878
1870
        :return: the header and the decompressor stream.
1879
1871
                 as (stream, header_record)
1880
1872
        """
1881
 
        df = tuned_gzip.GzipFile(mode='rb', fileobj=StringIO(raw_data))
 
1873
        df = gzip.GzipFile(mode='rb', fileobj=StringIO(raw_data))
1882
1874
        try:
1883
1875
            # Current serialise
1884
1876
            rec = self._check_header(key, df.readline())
1893
1885
        # 4168 calls in 2880 217 internal
1894
1886
        # 4168 calls to _parse_record_header in 2121
1895
1887
        # 4168 calls to readlines in 330
1896
 
        df = tuned_gzip.GzipFile(mode='rb', fileobj=StringIO(data))
 
1888
        df = gzip.GzipFile(mode='rb', fileobj=StringIO(data))
1897
1889
        try:
1898
1890
            record_contents = df.readlines()
1899
1891
        except Exception, e:
1921
1913
        The result will be returned in whatever is the fastest to read.
1922
1914
        Not by the order requested. Also, multiple requests for the same
1923
1915
        record will only yield 1 response.
 
1916
 
1924
1917
        :param records: A list of (key, access_memo) entries
1925
1918
        :return: Yields (key, contents, digest) in the order
1926
1919
                 read, not the order requested
1984
1977
        :param key: The key of the record. Currently keys are always serialised
1985
1978
            using just the trailing component.
1986
1979
        :param dense_lines: The bytes of lines but in a denser form. For
1987
 
            instance, if lines is a list of 1000 bytestrings each ending in \n,
1988
 
            dense_lines may be a list with one line in it, containing all the
1989
 
            1000's lines and their \n's. Using dense_lines if it is already
1990
 
            known is a win because the string join to create bytes in this
1991
 
            function spends less time resizing the final string.
 
1980
            instance, if lines is a list of 1000 bytestrings each ending in
 
1981
            \\n, dense_lines may be a list with one line in it, containing all
 
1982
            the 1000's lines and their \\n's. Using dense_lines if it is
 
1983
            already known is a win because the string join to create bytes in
 
1984
            this function spends less time resizing the final string.
1992
1985
        :return: (len, a StringIO instance with the raw data ready to read.)
1993
1986
        """
1994
1987
        chunks = ["version %s %d %s\n" % (key[-1], len(lines), digest)]
2014
2007
        """See VersionedFiles.keys."""
2015
2008
        if 'evil' in debug.debug_flags:
2016
2009
            trace.mutter_callsite(2, "keys scales with size of history")
2017
 
        sources = [self._index] + self._fallback_vfs
 
2010
        sources = [self._index] + self._immediate_fallback_vfs
2018
2011
        result = set()
2019
2012
        for source in sources:
2020
2013
            result.update(source.keys())
2060
2053
 
2061
2054
        missing_keys = set(nonlocal_keys)
2062
2055
        # Read from remote versioned file instances and provide to our caller.
2063
 
        for source in self.vf._fallback_vfs:
 
2056
        for source in self.vf._immediate_fallback_vfs:
2064
2057
            if not missing_keys:
2065
2058
                break
2066
2059
            # Loop over fallback repositories asking them for texts - ignore
2785
2778
        return key[:-1], key[-1]
2786
2779
 
2787
2780
 
2788
 
class _KeyRefs(object):
2789
 
 
2790
 
    def __init__(self, track_new_keys=False):
2791
 
        # dict mapping 'key' to 'set of keys referring to that key'
2792
 
        self.refs = {}
2793
 
        if track_new_keys:
2794
 
            # set remembering all new keys
2795
 
            self.new_keys = set()
2796
 
        else:
2797
 
            self.new_keys = None
2798
 
 
2799
 
    def clear(self):
2800
 
        if self.refs:
2801
 
            self.refs.clear()
2802
 
        if self.new_keys:
2803
 
            self.new_keys.clear()
2804
 
 
2805
 
    def add_references(self, key, refs):
2806
 
        # Record the new references
2807
 
        for referenced in refs:
2808
 
            try:
2809
 
                needed_by = self.refs[referenced]
2810
 
            except KeyError:
2811
 
                needed_by = self.refs[referenced] = set()
2812
 
            needed_by.add(key)
2813
 
        # Discard references satisfied by the new key
2814
 
        self.add_key(key)
2815
 
 
2816
 
    def get_new_keys(self):
2817
 
        return self.new_keys
2818
 
    
2819
 
    def get_unsatisfied_refs(self):
2820
 
        return self.refs.iterkeys()
2821
 
 
2822
 
    def _satisfy_refs_for_key(self, key):
2823
 
        try:
2824
 
            del self.refs[key]
2825
 
        except KeyError:
2826
 
            # No keys depended on this key.  That's ok.
2827
 
            pass
2828
 
 
2829
 
    def add_key(self, key):
2830
 
        # satisfy refs for key, and remember that we've seen this key.
2831
 
        self._satisfy_refs_for_key(key)
2832
 
        if self.new_keys is not None:
2833
 
            self.new_keys.add(key)
2834
 
 
2835
 
    def satisfy_refs_for_keys(self, keys):
2836
 
        for key in keys:
2837
 
            self._satisfy_refs_for_key(key)
2838
 
 
2839
 
    def get_referrers(self):
2840
 
        result = set()
2841
 
        for referrers in self.refs.itervalues():
2842
 
            result.update(referrers)
2843
 
        return result
2844
 
 
2845
 
 
2846
2781
class _KnitGraphIndex(object):
2847
2782
    """A KnitVersionedFiles index layered on GraphIndex."""
2848
2783
 
3277
3212
                yield data
3278
3213
 
3279
3214
 
3280
 
class _DirectPackAccess(object):
3281
 
    """Access to data in one or more packs with less translation."""
3282
 
 
3283
 
    def __init__(self, index_to_packs, reload_func=None, flush_func=None):
3284
 
        """Create a _DirectPackAccess object.
3285
 
 
3286
 
        :param index_to_packs: A dict mapping index objects to the transport
3287
 
            and file names for obtaining data.
3288
 
        :param reload_func: A function to call if we determine that the pack
3289
 
            files have moved and we need to reload our caches. See
3290
 
            bzrlib.repo_fmt.pack_repo.AggregateIndex for more details.
3291
 
        """
3292
 
        self._container_writer = None
3293
 
        self._write_index = None
3294
 
        self._indices = index_to_packs
3295
 
        self._reload_func = reload_func
3296
 
        self._flush_func = flush_func
3297
 
 
3298
 
    def add_raw_records(self, key_sizes, raw_data):
3299
 
        """Add raw knit bytes to a storage area.
3300
 
 
3301
 
        The data is spooled to the container writer in one bytes-record per
3302
 
        raw data item.
3303
 
 
3304
 
        :param sizes: An iterable of tuples containing the key and size of each
3305
 
            raw data segment.
3306
 
        :param raw_data: A bytestring containing the data.
3307
 
        :return: A list of memos to retrieve the record later. Each memo is an
3308
 
            opaque index memo. For _DirectPackAccess the memo is (index, pos,
3309
 
            length), where the index field is the write_index object supplied
3310
 
            to the PackAccess object.
3311
 
        """
3312
 
        if type(raw_data) is not str:
3313
 
            raise AssertionError(
3314
 
                'data must be plain bytes was %s' % type(raw_data))
3315
 
        result = []
3316
 
        offset = 0
3317
 
        for key, size in key_sizes:
3318
 
            p_offset, p_length = self._container_writer.add_bytes_record(
3319
 
                raw_data[offset:offset+size], [])
3320
 
            offset += size
3321
 
            result.append((self._write_index, p_offset, p_length))
3322
 
        return result
3323
 
 
3324
 
    def flush(self):
3325
 
        """Flush pending writes on this access object.
3326
 
 
3327
 
        This will flush any buffered writes to a NewPack.
3328
 
        """
3329
 
        if self._flush_func is not None:
3330
 
            self._flush_func()
3331
 
            
3332
 
    def get_raw_records(self, memos_for_retrieval):
3333
 
        """Get the raw bytes for a records.
3334
 
 
3335
 
        :param memos_for_retrieval: An iterable containing the (index, pos,
3336
 
            length) memo for retrieving the bytes. The Pack access method
3337
 
            looks up the pack to use for a given record in its index_to_pack
3338
 
            map.
3339
 
        :return: An iterator over the bytes of the records.
3340
 
        """
3341
 
        # first pass, group into same-index requests
3342
 
        request_lists = []
3343
 
        current_index = None
3344
 
        for (index, offset, length) in memos_for_retrieval:
3345
 
            if current_index == index:
3346
 
                current_list.append((offset, length))
3347
 
            else:
3348
 
                if current_index is not None:
3349
 
                    request_lists.append((current_index, current_list))
3350
 
                current_index = index
3351
 
                current_list = [(offset, length)]
3352
 
        # handle the last entry
3353
 
        if current_index is not None:
3354
 
            request_lists.append((current_index, current_list))
3355
 
        for index, offsets in request_lists:
3356
 
            try:
3357
 
                transport, path = self._indices[index]
3358
 
            except KeyError:
3359
 
                # A KeyError here indicates that someone has triggered an index
3360
 
                # reload, and this index has gone missing, we need to start
3361
 
                # over.
3362
 
                if self._reload_func is None:
3363
 
                    # If we don't have a _reload_func there is nothing that can
3364
 
                    # be done
3365
 
                    raise
3366
 
                raise errors.RetryWithNewPacks(index,
3367
 
                                               reload_occurred=True,
3368
 
                                               exc_info=sys.exc_info())
3369
 
            try:
3370
 
                reader = pack.make_readv_reader(transport, path, offsets)
3371
 
                for names, read_func in reader.iter_records():
3372
 
                    yield read_func(None)
3373
 
            except errors.NoSuchFile:
3374
 
                # A NoSuchFile error indicates that a pack file has gone
3375
 
                # missing on disk, we need to trigger a reload, and start over.
3376
 
                if self._reload_func is None:
3377
 
                    raise
3378
 
                raise errors.RetryWithNewPacks(transport.abspath(path),
3379
 
                                               reload_occurred=False,
3380
 
                                               exc_info=sys.exc_info())
3381
 
 
3382
 
    def set_writer(self, writer, index, transport_packname):
3383
 
        """Set a writer to use for adding data."""
3384
 
        if index is not None:
3385
 
            self._indices[index] = transport_packname
3386
 
        self._container_writer = writer
3387
 
        self._write_index = index
3388
 
 
3389
 
    def reload_or_raise(self, retry_exc):
3390
 
        """Try calling the reload function, or re-raise the original exception.
3391
 
 
3392
 
        This should be called after _DirectPackAccess raises a
3393
 
        RetryWithNewPacks exception. This function will handle the common logic
3394
 
        of determining when the error is fatal versus being temporary.
3395
 
        It will also make sure that the original exception is raised, rather
3396
 
        than the RetryWithNewPacks exception.
3397
 
 
3398
 
        If this function returns, then the calling function should retry
3399
 
        whatever operation was being performed. Otherwise an exception will
3400
 
        be raised.
3401
 
 
3402
 
        :param retry_exc: A RetryWithNewPacks exception.
3403
 
        """
3404
 
        is_error = False
3405
 
        if self._reload_func is None:
3406
 
            is_error = True
3407
 
        elif not self._reload_func():
3408
 
            # The reload claimed that nothing changed
3409
 
            if not retry_exc.reload_occurred:
3410
 
                # If there wasn't an earlier reload, then we really were
3411
 
                # expecting to find changes. We didn't find them, so this is a
3412
 
                # hard error
3413
 
                is_error = True
3414
 
        if is_error:
3415
 
            exc_class, exc_value, exc_traceback = retry_exc.exc_info
3416
 
            raise exc_class, exc_value, exc_traceback
3417
 
 
3418
 
 
3419
 
# Deprecated, use PatienceSequenceMatcher instead
3420
 
KnitSequenceMatcher = patiencediff.PatienceSequenceMatcher
3421
 
 
3422
 
 
3423
3215
def annotate_knit(knit, revision_id):
3424
3216
    """Annotate a knit with no cached annotations.
3425
3217
 
3523
3315
        return records, ann_keys
3524
3316
 
3525
3317
    def _get_needed_texts(self, key, pb=None):
3526
 
        # if True or len(self._vf._fallback_vfs) > 0:
3527
 
        if len(self._vf._fallback_vfs) > 0:
 
3318
        # if True or len(self._vf._immediate_fallback_vfs) > 0:
 
3319
        if len(self._vf._immediate_fallback_vfs) > 0:
3528
3320
            # If we have fallbacks, go to the generic path
3529
3321
            for v in annotate.Annotator._get_needed_texts(self, key, pb=pb):
3530
3322
                yield v
3535
3327
                for idx, (sub_key, text, num_lines) in enumerate(
3536
3328
                                                self._extract_texts(records)):
3537
3329
                    if pb is not None:
3538
 
                        pb.update('annotating', idx, len(records))
 
3330
                        pb.update(gettext('annotating'), idx, len(records))
3539
3331
                    yield sub_key, text, num_lines
3540
3332
                for sub_key in ann_keys:
3541
3333
                    text = self._text_cache[sub_key]