1# Copyright (C) 2008, 2009, 2011 Canonical Ltd 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 17"""Working tree content filtering support. 18 19A filter consists of a read converter, write converter pair. 20The content in the working tree is called the convenience format 21while the content actually stored is called the canonical format. 22The read converter produces canonical content from convenience 23content while the writer goes the other way. 24 25Converters have the following signatures:: 26 27 read_converter(chunks) -> chunks 28 write_converter(chunks, context) -> chunks 29 30where: 31 32 * chunks is an iterator over a sequence of byte strings 33 34 * context is an optional ContentFilterContent object (possibly None) 35 providing converters access to interesting information, e.g. the 36 relative path of the file. 37 38Note that context is currently only supported for write converters. 39""" 40 41 42from io import ( 43 BytesIO, 44 ) 45 46from ..lazy_import import lazy_import 47lazy_import(globals(), """ 48from breezy import ( 49 config, 50 osutils, 51 registry, 52 ) 53""") 54 55from .. import ( 56 errors, 57 ) 58 59 60class ContentFilter(object): 61 62 def __init__(self, reader, writer): 63 """Create a filter that converts content while reading and writing. 64 65 :param reader: function for converting convenience to canonical content 66 :param writer: function for converting canonical to convenience content 67 """ 68 self.reader = reader 69 self.writer = writer 70 71 def __repr__(self): 72 return "reader: %s, writer: %s" % (self.reader, self.writer) 73 74 75class ContentFilterContext(object): 76 """Object providing information that filters can use.""" 77 78 def __init__(self, relpath=None, tree=None): 79 """Create a context. 80 81 :param relpath: the relative path or None if this context doesn't 82 support that information. 83 :param tree: the Tree providing this file or None if this context 84 doesn't support that information. 85 """ 86 self._relpath = relpath 87 self._tree = tree 88 # Cached values 89 self._revision_id = None 90 self._revision = None 91 92 def relpath(self): 93 """Relative path of file to tree-root.""" 94 return self._relpath 95 96 def source_tree(self): 97 """Source Tree object.""" 98 return self._tree 99 100 def revision_id(self): 101 """Id of revision that last changed this file.""" 102 if self._revision_id is None: 103 if self._tree is not None: 104 self._revision_id = self._tree.get_file_revision( 105 self._relpath) 106 return self._revision_id 107 108 def revision(self): 109 """Revision this variation of the file was introduced in.""" 110 if self._revision is None: 111 rev_id = self.revision_id() 112 if rev_id is not None: 113 repo = getattr(self._tree, '_repository', None) 114 if repo is None: 115 repo = self._tree.branch.repository 116 self._revision = repo.get_revision(rev_id) 117 return self._revision 118 119 120def filtered_input_file(f, filters): 121 """Get an input file that converts external to internal content. 122 123 :param f: the original input file 124 :param filters: the stack of filters to apply 125 :return: a file-like object, size 126 """ 127 chunks = [f.read()] 128 for filter in filters: 129 if filter.reader is not None: 130 chunks = filter.reader(chunks) 131 text = b''.join(chunks) 132 return BytesIO(text), len(text) 133 134 135def filtered_output_bytes(chunks, filters, context=None): 136 """Convert byte chunks from internal to external format. 137 138 :param chunks: an iterator containing the original content 139 :param filters: the stack of filters to apply 140 :param context: a ContentFilterContext object passed to 141 each filter 142 :return: an iterator containing the content to output 143 """ 144 if filters: 145 for filter in reversed(filters): 146 if filter.writer is not None: 147 chunks = filter.writer(chunks, context) 148 return chunks 149 150 151def internal_size_sha_file_byname(name, filters): 152 """Get size and sha of internal content given external content. 153 154 :param name: path to file 155 :param filters: the stack of filters to apply 156 """ 157 with open(name, 'rb', 65000) as f: 158 if filters: 159 f, size = filtered_input_file(f, filters) 160 return osutils.size_sha_file(f) 161 162 163class FilteredStat(object): 164 165 def __init__(self, base, st_size=None): 166 self.st_mode = base.st_mode 167 self.st_size = st_size or base.st_size 168 self.st_mtime = base.st_mtime 169 self.st_ctime = base.st_ctime 170 171 172# The registry of filter stacks indexed by name. 173filter_stacks_registry = registry.Registry() 174 175 176# Cache of preferences -> stack 177# TODO: make this per branch (say) rather than global 178_stack_cache = {} 179 180 181def _get_registered_names(): 182 """Get the list of names with filters registered.""" 183 # Note: We may want to intelligently order these later. 184 # If so, the register_ fn will need to support an optional priority. 185 return filter_stacks_registry.keys() 186 187 188def _get_filter_stack_for(preferences): 189 """Get the filter stack given a sequence of preferences. 190 191 :param preferences: a sequence of (name,value) tuples where 192 name is the preference name and 193 value is the key into the filter stack map registered 194 for that preference. 195 """ 196 if preferences is None: 197 return [] 198 stack = _stack_cache.get(preferences) 199 if stack is not None: 200 return stack 201 stack = [] 202 for k, v in preferences: 203 if v is None: 204 continue 205 try: 206 stack_map_lookup = filter_stacks_registry.get(k) 207 except KeyError: 208 # Some preferences may not have associated filters 209 continue 210 items = stack_map_lookup(v) 211 if items: 212 stack.extend(items) 213 _stack_cache[preferences] = stack 214 return stack 215 216 217def _reset_registry(value=None): 218 """Reset the filter stack registry. 219 220 This function is provided to aid testing. The expected usage is:: 221 222 old = _reset_registry() 223 # run tests 224 _reset_registry(old) 225 226 :param value: the value to set the registry to or None for an empty one. 227 :return: the existing value before it reset. 228 """ 229 global filter_stacks_registry 230 original = filter_stacks_registry 231 if value is None: 232 filter_stacks_registry = registry.Registry() 233 else: 234 filter_stacks_registry = value 235 _stack_cache.clear() 236 return original 237 238 239filter_stacks_registry.register_lazy('eol', 'breezy.filters.eol', 'eol_lookup') 240