1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
7 from docs_server_utils
import ToUnicode
8 from file_system
import FileNotFoundError
9 from future
import Future
10 from path_util
import AssertIsDirectory
, AssertIsFile
, ToDirectory
11 from third_party
.json_schema_compiler
import json_parse
12 from third_party
.json_schema_compiler
.memoize
import memoize
13 from third_party
.motemplate
import Motemplate
16 _CACHEABLE_FUNCTIONS
= set()
17 _SINGLE_FILE_FUNCTIONS
= set()
20 def _GetUnboundFunction(fn
):
21 '''Functions bound to an object are separate from the unbound
22 defintion. This causes issues when checking for cache membership,
23 so always get the unbound function, if possible.
25 return getattr(fn
, 'im_func', fn
)
29 '''A decorator which can be applied to the compilation function
30 passed to CompiledFileSystem.Create, indicating that file/list data
33 This decorator should be listed first in any list of decorators, along
34 with the SingleFile decorator below.
36 _CACHEABLE_FUNCTIONS
.add(_GetUnboundFunction(fn
))
41 '''A decorator which can be optionally applied to the compilation function
42 passed to CompiledFileSystem.Create, indicating that the function only
43 needs access to the file which is given in the function's callback. When
44 this is the case some optimisations can be done.
46 Note that this decorator must be listed first in any list of decorators to
49 _SINGLE_FILE_FUNCTIONS
.add(_GetUnboundFunction(fn
))
54 '''A decorator which can be optionally applied to the compilation function
55 passed to CompiledFileSystem.Create, indicating that the function processes
56 the file's data as Unicode text.
59 # The arguments passed to fn can be (self, path, data) or (path, data). In
60 # either case the last argument is |data|, which should be converted to
62 def convert_args(args
):
64 args
[-1] = ToUnicode(args
[-1])
67 return lambda *args
: fn(*convert_args(args
))
70 class _CacheEntry(object):
71 def __init__(self
, cache_data
, version
):
73 self
.cache_data
= cache_data
74 self
.version
= version
77 class CompiledFileSystem(object):
78 '''This class caches FileSystem data that has been processed.
81 class Factory(object):
82 '''A class to build a CompiledFileSystem backed by |file_system|.
85 def __init__(self
, object_store_creator
):
86 self
._object
_store
_creator
= object_store_creator
88 def Create(self
, file_system
, compilation_function
, cls
, category
=None):
89 '''Creates a CompiledFileSystem view over |file_system| that populates
90 its cache by calling |compilation_function| with (path, data), where
91 |data| is the data that was fetched from |path| in |file_system|.
93 The namespace for the compiled file system is derived similar to
94 ObjectStoreCreator: from |cls| along with an optional |category|.
96 assert isinstance(cls
, type)
97 assert not cls
.__name
__[0].islower() # guard against non-class types
98 full_name
= [cls
.__name
__, file_system
.GetIdentity()]
99 if category
is not None:
100 full_name
.append(category
)
101 def create_object_store(my_category
):
102 # The read caches can start populated (start_empty=False) because file
103 # updates are picked up by the stat - but only if the compilation
104 # function is affected by a single file. If the compilation function is
105 # affected by other files (e.g. compiling a list of APIs available to
106 # extensions may be affected by both a features file and the list of
107 # files in the API directory) then this optimisation won't work.
108 return self
._object
_store
_creator
.Create(
110 category
='/'.join(full_name
+ [my_category
]),
111 start_empty
=compilation_function
not in _SINGLE_FILE_FUNCTIONS
)
112 return CompiledFileSystem(file_system
,
113 compilation_function
,
114 create_object_store('file'),
115 create_object_store('list'))
118 def ForJson(self
, file_system
):
119 '''A CompiledFileSystem specifically for parsing JSON configuration data.
120 These are memoized over file systems tied to different branches.
122 return self
.Create(file_system
,
123 Cache(SingleFile(lambda _
, data
:
124 json_parse
.Parse(ToUnicode(data
)))),
129 def ForTemplates(self
, file_system
):
130 '''Creates a CompiledFileSystem for parsing templates.
134 Cache(SingleFile(lambda path
, text
:
135 Motemplate(ToUnicode(text
), name
=path
))),
139 def ForUnicode(self
, file_system
):
140 '''Creates a CompiledFileSystem for Unicode text processing.
144 SingleFile(lambda _
, text
: ToUnicode(text
)),
150 compilation_function
,
153 self
._file
_system
= file_system
154 self
._compilation
_function
= compilation_function
155 self
._file
_object
_store
= file_object_store
156 self
._list
_object
_store
= list_object_store
158 def _Get(self
, store
, key
):
159 if _GetUnboundFunction(self
._compilation
_function
) in _CACHEABLE_FUNCTIONS
:
160 return store
.Get(key
)
161 return Future(value
=None)
163 def _Set(self
, store
, key
, value
):
164 if _GetUnboundFunction(self
._compilation
_function
) in _CACHEABLE_FUNCTIONS
:
165 store
.Set(key
, value
)
167 def _RecursiveList(self
, path
):
168 '''Returns a Future containing the recursive directory listing of |path| as
169 a flat list of paths.
171 def split_dirs_from_files(paths
):
172 '''Returns a tuple (dirs, files) where |dirs| contains the directory
173 names in |paths| and |files| contains the files.
177 result
[0 if path
.endswith('/') else 1].append(path
)
180 def add_prefix(prefix
, paths
):
181 return [prefix
+ path
for path
in paths
]
183 # Read in the initial list of files. Do this eagerly (i.e. not part of the
184 # asynchronous Future contract) because there's a greater chance to
185 # parallelise fetching with the second layer (can fetch multiple paths).
187 first_layer_dirs
, first_layer_files
= split_dirs_from_files(
188 self
._file
_system
.ReadSingle(path
).Get())
189 except FileNotFoundError
:
190 return Future(exc_info
=sys
.exc_info())
192 if not first_layer_dirs
:
193 return Future(value
=first_layer_files
)
195 def get_from_future_listing(listings
):
196 '''Recursively lists files from directory listing |futures|.
199 for dir_name
, listing
in listings
.iteritems():
200 new_dirs
, new_files
= split_dirs_from_files(listing
)
201 # |dirs| are paths for reading. Add the full prefix relative to
202 # |path| so that |file_system| can find the files.
203 dirs
+= add_prefix(dir_name
, new_dirs
)
204 # |files| are not for reading, they are for returning to the caller.
205 # This entire function set (i.e. GetFromFileListing) is defined to
206 # not include the fetched-path in the result, however, |dir_name|
207 # will be prefixed with |path|. Strip it.
208 assert dir_name
.startswith(path
)
209 files
+= add_prefix(dir_name
[len(path
):], new_files
)
211 files
+= self
._file
_system
.Read(dirs
).Then(
212 get_from_future_listing
).Get()
215 return self
._file
_system
.Read(add_prefix(path
, first_layer_dirs
)).Then(
216 lambda results
: first_layer_files
+ get_from_future_listing(results
))
218 def GetFromFile(self
, path
, skip_not_found
=False):
219 '''Calls |compilation_function| on the contents of the file at |path|.
220 If |skip_not_found| is True, then None is passed to |compilation_function|.
225 version
= self
._file
_system
.Stat(path
).version
226 except FileNotFoundError
:
230 return Future(exc_info
=sys
.exc_info())
232 cache_entry
= self
._Get
(self
._file
_object
_store
, path
).Get()
233 if (cache_entry
is not None) and (version
== cache_entry
.version
):
234 return Future(value
=cache_entry
.cache_data
)
237 cache_data
= self
._compilation
_function
(path
, files
)
238 self
._Set
(self
._file
_object
_store
, path
, _CacheEntry(cache_data
, version
))
241 return self
._file
_system
.ReadSingle(
242 path
, skip_not_found
=skip_not_found
).Then(compile_
)
244 def GetFromFileListing(self
, path
):
245 '''Calls |compilation_function| on the listing of the files at |path|.
246 Assumes that the path given is to a directory.
248 AssertIsDirectory(path
)
251 version
= self
._file
_system
.Stat(path
).version
252 except FileNotFoundError
:
253 return Future(exc_info
=sys
.exc_info())
255 cache_entry
= self
._Get
(self
._list
_object
_store
, path
).Get()
256 if (cache_entry
is not None) and (version
== cache_entry
.version
):
257 return Future(value
=cache_entry
.cache_data
)
260 cache_data
= self
._compilation
_function
(path
, files
)
261 self
._Set
(self
._list
_object
_store
, path
, _CacheEntry(cache_data
, version
))
263 return self
._RecursiveList
(path
).Then(compile_
)
265 # _GetFileVersionFromCache and _GetFileListingVersionFromCache are exposed
266 # *only* so that ChainedCompiledFileSystem can optimise its caches. *Do not*
267 # use these methods otherwise, they don't do what you want. Use
268 # FileSystem.Stat on the FileSystem that this CompiledFileSystem uses.
270 def _GetFileVersionFromCache(self
, path
):
271 cache_entry
= self
._Get
(self
._file
_object
_store
, path
).Get()
272 if cache_entry
is not None:
273 return Future(value
=cache_entry
.version
)
274 stat_future
= self
._file
_system
.StatAsync(path
)
275 return Future(callback
=lambda: stat_future
.Get().version
)
277 def _GetFileListingVersionFromCache(self
, path
):
278 path
= ToDirectory(path
)
279 cache_entry
= self
._Get
(self
._list
_object
_store
, path
).Get()
280 if cache_entry
is not None:
281 return Future(value
=cache_entry
.version
)
282 stat_future
= self
._file
_system
.StatAsync(path
)
283 return Future(callback
=lambda: stat_future
.Get().version
)
285 def GetIdentity(self
):
286 return self
._file
_system
.GetIdentity()