blob: 78c7cca9cb7e6d9385fd954ce3512ce13ffd2da6 [file] [log] [blame]
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
dpranke@chromium.org2a009622011-03-01 02:43:31 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
dpranke@chromium.org17cc2442012-10-17 21:12:09 +00005"""A database of OWNERS files.
6
7OWNERS files indicate who is allowed to approve changes in a specific directory
8(or who is allowed to make changes without needing approval of another OWNER).
9Note that all changes must still be reviewed by someone familiar with the code,
10so you may need approval from both an OWNER and a reviewer in many cases.
11
12The syntax of the OWNERS file is, roughly:
13
14lines := (\s* line? \s* "\n")*
15
16line := directive
dpranke@chromium.orgd16e48b2012-12-03 21:53:49 +000017 | "per-file" \s+ glob \s* "=" \s* directive
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000018 | comment
19
20directive := "set noparent"
peter@chromium.org2ce13132015-04-16 16:42:08 +000021 | "file:" glob
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000022 | email_address
23 | "*"
24
25glob := [a-zA-Z0-9_-*?]+
26
27comment := "#" [^"\n"]*
28
29Email addresses must follow the foo@bar.com short form (exact syntax given
30in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix
31shell conventions, and relative and absolute paths are not allowed (i.e.,
32globs only refer to the files in the current directory).
33
34If a user's email is one of the email_addresses in the file, the user is
35considered an "OWNER" for all files in the directory.
36
37If the "per-file" directive is used, the line only applies to files in that
38directory that match the filename glob specified.
39
40If the "set noparent" directive used, then only entries in this OWNERS file
41apply to files in this directory; if the "set noparent" directive is not
42used, then entries in OWNERS files in enclosing (upper) directories also
43apply (up until a "set noparent is encountered").
44
45If "per-file glob=set noparent" is used, then global directives are ignored
46for the glob, and only the "per-file" owners are used for files matching that
47glob.
48
peter@chromium.org2ce13132015-04-16 16:42:08 +000049If the "file:" directive is used, the referred to OWNERS file will be parsed and
50considered when determining the valid set of OWNERS. If the filename starts with
51"//" it is relative to the root of the repository, otherwise it is relative to
52the current file
53
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000054Examples for all of these combinations can be found in tests/owners_unittest.py.
55"""
dpranke@chromium.org2a009622011-03-01 02:43:31 +000056
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +000057import collections
dtu944b6052016-07-14 14:48:21 -070058import fnmatch
dpranke@chromium.orgc591a702012-12-20 20:14:58 +000059import random
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +000060import re
61
62
63# If this is present by itself on a line, this means that everyone can review.
64EVERYONE = '*'
65
66
67# Recognizes 'X@Y' email addresses. Very simplistic.
68BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$'
dpranke@chromium.org2a009622011-03-01 02:43:31 +000069
dpranke@chromium.org2a009622011-03-01 02:43:31 +000070
dpranke@chromium.org923950f2011-03-17 23:40:00 +000071def _assert_is_collection(obj):
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000072 assert not isinstance(obj, basestring)
maruel@chromium.org725f1c32011-04-01 20:24:54 +000073 # Module 'collections' has no 'Iterable' member
74 # pylint: disable=E1101
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000075 if hasattr(collections, 'Iterable') and hasattr(collections, 'Sized'):
76 assert (isinstance(obj, collections.Iterable) and
77 isinstance(obj, collections.Sized))
dpranke@chromium.org923950f2011-03-17 23:40:00 +000078
79
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000080class SyntaxErrorInOwnersFile(Exception):
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000081 def __init__(self, path, lineno, msg):
82 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg))
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000083 self.path = path
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000084 self.lineno = lineno
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000085 self.msg = msg
86
87 def __str__(self):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000088 return '%s:%d syntax error: %s' % (self.path, self.lineno, self.msg)
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000089
90
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000091class Database(object):
92 """A database of OWNERS files for a repository.
93
94 This class allows you to find a suggested set of reviewers for a list
95 of changed files, and see if a list of changed files is covered by a
96 list of reviewers."""
97
dtu944b6052016-07-14 14:48:21 -070098 def __init__(self, root, fopen, os_path):
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000099 """Args:
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000100 root: the path to the root of the Repository
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000101 open: function callback to open a text file for reading
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000102 os_path: module/object callback with fields for 'abspath', 'dirname',
mbjorgef2d73522016-07-14 13:28:59 -0700103 'exists', 'join', and 'relpath'
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000104 """
105 self.root = root
106 self.fopen = fopen
107 self.os_path = os_path
108
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000109 # Pick a default email regexp to use; callers can override as desired.
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000110 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000111
dtu944b6052016-07-14 14:48:21 -0700112 # Mapping of owners to the paths or globs they own.
113 self._owners_to_paths = {EVERYONE: set()}
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000114
115 # Mapping of paths to authorized owners.
dtu944b6052016-07-14 14:48:21 -0700116 self._paths_to_owners = {}
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000117
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000118 # Mapping reviewers to the preceding comment per file in the OWNERS files.
119 self.comments = {}
120
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000121 # Set of paths that stop us from looking above them for owners.
122 # (This is implicitly true for the root directory).
dtu944b6052016-07-14 14:48:21 -0700123 self._stop_looking = set([''])
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000124
peter@chromium.org2ce13132015-04-16 16:42:08 +0000125 # Set of files which have already been read.
126 self.read_files = set()
127
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000128 def reviewers_for(self, files, author):
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000129 """Returns a suggested set of reviewers that will cover the files.
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000130
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000131 files is a sequence of paths relative to (and under) self.root.
132 If author is nonempty, we ensure it is not included in the set returned
133 in order avoid suggesting the author as a reviewer for their own changes."""
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000134 self._check_paths(files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000135 self.load_data_needed_for(files)
dtu944b6052016-07-14 14:48:21 -0700136
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000137 suggested_owners = self._covering_set_of_owners_for(files, author)
dpranke@chromium.org9d66f482013-01-18 02:57:11 +0000138 if EVERYONE in suggested_owners:
139 if len(suggested_owners) > 1:
140 suggested_owners.remove(EVERYONE)
141 else:
142 suggested_owners = set(['<anyone>'])
143 return suggested_owners
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000144
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000145 def files_not_covered_by(self, files, reviewers):
146 """Returns the files not owned by one of the reviewers.
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000147
148 Args:
149 files is a sequence of paths relative to (and under) self.root.
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000150 reviewers is a sequence of strings matching self.email_regexp.
151 """
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000152 self._check_paths(files)
153 self._check_reviewers(reviewers)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000154 self.load_data_needed_for(files)
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000155
dtu944b6052016-07-14 14:48:21 -0700156 return set(f for f in files if not self._is_obj_covered_by(f, reviewers))
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000157
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000158 def _check_paths(self, files):
159 def _is_under(f, pfx):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000160 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx)
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000161 _assert_is_collection(files)
dpranke@chromium.orgb54a78e2012-12-13 23:37:23 +0000162 assert all(not self.os_path.isabs(f) and
163 _is_under(f, self.os_path.abspath(self.root)) for f in files)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000164
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000165 def _check_reviewers(self, reviewers):
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000166 _assert_is_collection(reviewers)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000167 assert all(self.email_regexp.match(r) for r in reviewers)
168
dtu944b6052016-07-14 14:48:21 -0700169 def _is_obj_covered_by(self, objname, reviewers):
170 reviewers = list(reviewers) + [EVERYONE]
171 while True:
172 for reviewer in reviewers:
173 for owned_pattern in self._owners_to_paths.get(reviewer, set()):
174 if fnmatch.fnmatch(objname, owned_pattern):
175 return True
176 if self._should_stop_looking(objname):
177 break
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000178 objname = self.os_path.dirname(objname)
dtu944b6052016-07-14 14:48:21 -0700179 return False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000180
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000181 def _enclosing_dir_with_owners(self, objname):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000182 """Returns the innermost enclosing directory that has an OWNERS file."""
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000183 dirpath = objname
dtu944b6052016-07-14 14:48:21 -0700184 while not self._owners_for(dirpath):
185 if self._should_stop_looking(dirpath):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000186 break
187 dirpath = self.os_path.dirname(dirpath)
188 return dirpath
189
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000190 def load_data_needed_for(self, files):
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000191 for f in files:
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000192 dirpath = self.os_path.dirname(f)
dtu944b6052016-07-14 14:48:21 -0700193 while not self._owners_for(dirpath):
peter@chromium.org2ce13132015-04-16 16:42:08 +0000194 self._read_owners(self.os_path.join(dirpath, 'OWNERS'))
dtu944b6052016-07-14 14:48:21 -0700195 if self._should_stop_looking(dirpath):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000196 break
197 dirpath = self.os_path.dirname(dirpath)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000198
dtu944b6052016-07-14 14:48:21 -0700199 def _should_stop_looking(self, objname):
200 return any(fnmatch.fnmatch(objname, stop_looking)
201 for stop_looking in self._stop_looking)
202
203 def _owners_for(self, objname):
204 obj_owners = set()
205 for owned_path, path_owners in self._paths_to_owners.iteritems():
206 if fnmatch.fnmatch(objname, owned_path):
207 obj_owners |= path_owners
208 return obj_owners
209
peter@chromium.org2ce13132015-04-16 16:42:08 +0000210 def _read_owners(self, path):
211 owners_path = self.os_path.join(self.root, path)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000212 if not self.os_path.exists(owners_path):
213 return
peter@chromium.org2ce13132015-04-16 16:42:08 +0000214
215 if owners_path in self.read_files:
216 return
217
218 self.read_files.add(owners_path)
219
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000220 comment = []
peter@chromium.org2ce13132015-04-16 16:42:08 +0000221 dirpath = self.os_path.dirname(path)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000222 in_comment = False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000223 lineno = 0
224 for line in self.fopen(owners_path):
225 lineno += 1
226 line = line.strip()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000227 if line.startswith('#'):
228 if not in_comment:
229 comment = []
230 comment.append(line[1:].strip())
231 in_comment = True
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000232 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000233 if line == '':
234 continue
235 in_comment = False
236
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000237 if line == 'set noparent':
dtu944b6052016-07-14 14:48:21 -0700238 self._stop_looking.add(dirpath)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000239 continue
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000240
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000241 m = re.match('per-file (.+)=(.+)', line)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000242 if m:
dpranke@chromium.orgd16e48b2012-12-03 21:53:49 +0000243 glob_string = m.group(1).strip()
244 directive = m.group(2).strip()
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000245 full_glob_string = self.os_path.join(self.root, dirpath, glob_string)
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000246 if '/' in glob_string or '\\' in glob_string:
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000247 raise SyntaxErrorInOwnersFile(owners_path, lineno,
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000248 'per-file globs cannot span directories or use escapes: "%s"' %
249 line)
dtu944b6052016-07-14 14:48:21 -0700250 relative_glob_string = self.os_path.relpath(full_glob_string, self.root)
251 self._add_entry(relative_glob_string, directive, 'per-file line',
252 owners_path, lineno, '\n'.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000253 continue
254
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000255 if line.startswith('set '):
256 raise SyntaxErrorInOwnersFile(owners_path, lineno,
257 'unknown option: "%s"' % line[4:].strip())
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000258
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000259 self._add_entry(dirpath, line, 'line', owners_path, lineno,
260 ' '.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000261
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000262 def _add_entry(self, path, directive,
263 line_type, owners_path, lineno, comment):
264 if directive == 'set noparent':
dtu944b6052016-07-14 14:48:21 -0700265 self._stop_looking.add(path)
peter@chromium.org2ce13132015-04-16 16:42:08 +0000266 elif directive.startswith('file:'):
267 owners_file = self._resolve_include(directive[5:], owners_path)
268 if not owners_file:
269 raise SyntaxErrorInOwnersFile(owners_path, lineno,
270 ('%s does not refer to an existing file.' % directive[5:]))
271
272 self._read_owners(owners_file)
273
274 dirpath = self.os_path.dirname(owners_file)
dtu944b6052016-07-14 14:48:21 -0700275 for key in self._owners_to_paths:
276 if not dirpath in self._owners_to_paths[key]:
peter@chromium.org2ce13132015-04-16 16:42:08 +0000277 continue
dtu944b6052016-07-14 14:48:21 -0700278 self._owners_to_paths[key].add(path)
peter@chromium.org2ce13132015-04-16 16:42:08 +0000279
dtu944b6052016-07-14 14:48:21 -0700280 if dirpath in self._paths_to_owners:
281 self._paths_to_owners.setdefault(path, set()).update(
282 self._paths_to_owners[dirpath])
peter@chromium.org2ce13132015-04-16 16:42:08 +0000283
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000284 elif self.email_regexp.match(directive) or directive == EVERYONE:
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000285 self.comments.setdefault(directive, {})
286 self.comments[directive][path] = comment
dtu944b6052016-07-14 14:48:21 -0700287 self._owners_to_paths.setdefault(directive, set()).add(path)
288 self._paths_to_owners.setdefault(path, set()).add(directive)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000289 else:
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000290 raise SyntaxErrorInOwnersFile(owners_path, lineno,
peter@chromium.org2ce13132015-04-16 16:42:08 +0000291 ('%s is not a "set" directive, file include, "*", '
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000292 'or an email address: "%s"' % (line_type, directive)))
293
peter@chromium.org2ce13132015-04-16 16:42:08 +0000294 def _resolve_include(self, path, start):
295 if path.startswith('//'):
296 include_path = path[2:]
297 else:
298 assert start.startswith(self.root)
mbjorgef2d73522016-07-14 13:28:59 -0700299 start = self.os_path.dirname(self.os_path.relpath(start, self.root))
peter@chromium.org2ce13132015-04-16 16:42:08 +0000300 include_path = self.os_path.join(start, path)
301
302 owners_path = self.os_path.join(self.root, include_path)
303 if not self.os_path.exists(owners_path):
304 return None
305
306 return include_path
307
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000308 def _covering_set_of_owners_for(self, files, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000309 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000310 all_possible_owners = self.all_possible_owners(dirs_remaining, author)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000311 suggested_owners = set()
312 while dirs_remaining:
313 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining)
314 suggested_owners.add(owner)
315 dirs_to_remove = set(el[0] for el in all_possible_owners[owner])
316 dirs_remaining -= dirs_to_remove
317 return suggested_owners
dpranke@chromium.org5e5d37b2012-12-19 21:04:58 +0000318
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000319 def all_possible_owners(self, dirs, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000320 """Returns a list of (potential owner, distance-from-dir) tuples; a
321 distance of 1 is the lowest/closest possible distance (which makes the
322 subsequent math easier)."""
323 all_possible_owners = {}
zork@chromium.org046e1752012-05-07 05:56:12 +0000324 for current_dir in dirs:
zork@chromium.org046e1752012-05-07 05:56:12 +0000325 dirname = current_dir
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000326 distance = 1
327 while True:
dtu944b6052016-07-14 14:48:21 -0700328 for owner in self._owners_for(dirname):
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000329 if author and owner == author:
330 continue
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000331 all_possible_owners.setdefault(owner, [])
332 # If the same person is in multiple OWNERS files above a given
333 # directory, only count the closest one.
334 if not any(current_dir == el[0] for el in all_possible_owners[owner]):
335 all_possible_owners[owner].append((current_dir, distance))
dtu944b6052016-07-14 14:48:21 -0700336 if self._should_stop_looking(dirname):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000337 break
338 dirname = self.os_path.dirname(dirname)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000339 distance += 1
340 return all_possible_owners
zork@chromium.org046e1752012-05-07 05:56:12 +0000341
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000342 @staticmethod
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000343 def total_costs_by_owner(all_possible_owners, dirs):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000344 # We want to minimize both the number of reviewers and the distance
345 # from the files/dirs needing reviews. The "pow(X, 1.75)" below is
346 # an arbitrarily-selected scaling factor that seems to work well - it
347 # will select one reviewer in the parent directory over three reviewers
348 # in subdirs, but not one reviewer over just two.
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000349 result = {}
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000350 for owner in all_possible_owners:
351 total_distance = 0
352 num_directories_owned = 0
353 for dirname, distance in all_possible_owners[owner]:
354 if dirname in dirs:
355 total_distance += distance
356 num_directories_owned += 1
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000357 if num_directories_owned:
358 result[owner] = (total_distance /
359 pow(num_directories_owned, 1.75))
360 return result
zork@chromium.org046e1752012-05-07 05:56:12 +0000361
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000362 @staticmethod
363 def lowest_cost_owner(all_possible_owners, dirs):
364 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners,
365 dirs)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000366 # Return the lowest cost owner. In the case of a tie, pick one randomly.
367 lowest_cost = min(total_costs_by_owner.itervalues())
368 lowest_cost_owners = filter(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000369 lambda owner: total_costs_by_owner[owner] == lowest_cost,
370 total_costs_by_owner)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000371 return random.Random().choice(lowest_cost_owners)