Add support for recursive requirements
Add a script to expand recursive requirements. This allows recursive
requirements to be tracked and updated; otherwise, we wouldnt know
about new requirements in included subfiles.
We do this because:
1. We cannot reliably check whether we need to update if we dont
expand included dependencies.
2. Not checking if we need to update and always updating adds 2+
seconds to each call to create_venv, which is also run under an
exclusive lock, significantly increasing the cost of virtualenv
commands.
The current implementation is a balance between simplicity,
performance, and maintainability. If more additions are added, we
should consider rewriting in Python.
BUG=chromium:645611
TEST=Run locally with recursive requirements.txt
Change-Id: Iffffa7348fade4113439fd4e372b3009090e2a47
Reviewed-on: https://chromium-review.googlesource.com/417248
Commit-Ready: Allen Li <ayatane@chromium.org>
Tested-by: Allen Li <ayatane@chromium.org>
Reviewed-by: Aviv Keshet <akeshet@chromium.org>
diff --git a/create_venv b/create_venv
index bc7934e..4aada2a 100755
--- a/create_venv
+++ b/create_venv
@@ -49,14 +49,14 @@
local venv_dir=$1
local requirements=$2
local installed="$venv_dir/.installed.txt"
- cmp -s "$requirements" "$installed"
+ cmp -s <("$basedir/expand_reqs.py" <"$requirements") "$installed"
}
mark_up_to_date() {
local venv_dir=$1
local requirements=$2
local installed="$venv_dir/.installed.txt"
- cp "$requirements" "$installed"
+ "$basedir/expand_reqs.py" <"$requirements" >"$installed"
}
init_venv() {
diff --git a/expand_reqs.py b/expand_reqs.py
new file mode 100755
index 0000000..a38043f
--- /dev/null
+++ b/expand_reqs.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+# Copyright 2016 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Expand recursive requirements in pip requirements.txt files.
+
+$ ./expand_reqs.py <requirements.txt
+
+Run tests using: python -m unittest expand_reqs
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+
+def main():
+ for line in expand_reqs(sys.stdin):
+ print line
+
+
+def expand_reqs(infile):
+ """Yield each line in infile, recursing into included requirements files."""
+ for line in infile:
+ line = line.strip()
+ yield line
+ requirements = _get_requirements_file(line)
+ if requirements and os.path.exists(requirements):
+ with open(requirements) as subfile:
+ for subline in expand_reqs(subfile):
+ yield subline
+ yield '# END %s' % line
+
+
+def _get_requirements_file(line):
+ """Return the requirements file specified by the input line.
+
+ A requirements line looks like: -r requirements.txt
+
+ Args:
+ line: String input
+
+ Returns:
+ Requirements file path as string if present, else an empty string.
+ """
+ parts = line.split()
+ if len(parts) == 2 and parts[0] == '-r':
+ return parts[1]
+ else:
+ return ''
+
+
+class TmpdirTestCase(unittest.TestCase):
+ """TestCase subclass providing a tmpdir fixture."""
+
+ def setUp(self):
+ self.tmpdir = tempfile.mkdtemp()
+ self._old_cwd = os.getcwd()
+ os.chdir(self.tmpdir)
+
+ def tearDown(self):
+ os.chdir(self._old_cwd)
+ shutil.rmtree(self.tmpdir)
+
+
+class ExpandReqsTestCase(TmpdirTestCase):
+ """Tests for expand_reqs()."""
+
+ def setUp(self):
+ super(ExpandReqsTestCase, self).setUp()
+ self.flat = 'flat.txt'
+ self.recursive = 'recursive.txt'
+ with open(self.flat, 'w') as f:
+ f.write('foo\nbar')
+ with open(self.recursive, 'w') as f:
+ f.write('spam\neggs\n-r flat.txt')
+
+ def test_no_recurse(self):
+ with open(self.flat) as f:
+ self.assertEqual(
+ list(expand_reqs(f)),
+ ['foo', 'bar'])
+
+ def test_recurse(self):
+ with open(self.recursive) as f:
+ self.assertEqual(
+ list(expand_reqs(f)),
+ ['spam', 'eggs', '-r flat.txt',
+ 'foo', 'bar', '# END -r flat.txt'])
+
+
+class GetRequirementsFileTestCase(unittest.TestCase):
+ """Tests for _get_requirements_file()."""
+
+ def test_no_match(self):
+ self.assertEqual(_get_requirements_file('foo'), '')
+
+ def test_match(self):
+ self.assertEqual(_get_requirements_file('-r foo.txt'), 'foo.txt')
+
+ def test_match_with_whitespace(self):
+ self.assertEqual(_get_requirements_file(' -r foo.txt \n'), 'foo.txt')
+
+
+if __name__ == '__main__':
+ main()