From ce8926a003661b5f30ce1730551440d86fe93b53 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Tue, 23 Oct 2018 06:09:33 +0200
Subject: [PATCH] feat: pip requirements.txt ignore (#2676)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Adds support for “# renovate: ignore” command in requirements.txt files.
---
 lib/manager/pip_requirements/extract.js       | 18 +++++++-
 lib/manager/pip_requirements/update.js        |  7 +++-
 .../pip_requirements/requirements3.txt        |  5 +++
 .../__snapshots__/extract.spec.js.snap        | 41 +++++++++++++++++++
 .../__snapshots__/update.spec.js.snap         | 20 +++++++++
 test/manager/pip_requirements/extract.spec.js |  9 ++++
 test/manager/pip_requirements/update.spec.js  | 19 ++++++++-
 7 files changed, 115 insertions(+), 4 deletions(-)
 create mode 100644 test/_fixtures/pip_requirements/requirements3.txt
 create mode 100644 test/manager/pip_requirements/__snapshots__/update.spec.js.snap

diff --git a/lib/manager/pip_requirements/extract.js b/lib/manager/pip_requirements/extract.js
index cd3e7a3367..5288056399 100644
--- a/lib/manager/pip_requirements/extract.js
+++ b/lib/manager/pip_requirements/extract.js
@@ -27,14 +27,28 @@ function extractDependencies(content) {
   const regex = new RegExp(`^(${packagePattern})(${specifierPattern})$`, 'g');
   const deps = content
     .split('\n')
-    .map((line, lineNumber) => {
+    .map((rawline, lineNumber) => {
+      let dep = {};
+      const [line, comment] = rawline.split('#').map(part => part.trim());
+      if (comment && comment.match(/^(renovate|pyup):/)) {
+        const command = comment
+          .split('#')[0]
+          .split(':')[1]
+          .trim();
+        if (command === 'ignore') {
+          dep.skipReason = 'ignored';
+        } else {
+          logger.info('Unknown pip_requirements command: ' + command);
+        }
+      }
       regex.lastIndex = 0;
       const matches = regex.exec(line);
       if (!matches) {
         return null;
       }
       const [, depName, currentValue] = matches;
-      const dep = {
+      dep = {
+        ...dep,
         depName,
         currentValue,
         lineNumber,
diff --git a/lib/manager/pip_requirements/update.js b/lib/manager/pip_requirements/update.js
index 99360c1002..8d8140ead1 100644
--- a/lib/manager/pip_requirements/update.js
+++ b/lib/manager/pip_requirements/update.js
@@ -6,7 +6,12 @@ function updateDependency(fileContent, upgrade) {
   try {
     logger.debug(`pip_requirements.updateDependency(): ${upgrade.newValue}`);
     const lines = fileContent.split('\n');
-    lines[upgrade.lineNumber] = `${upgrade.depName}${upgrade.newValue}`;
+    const oldValue = lines[upgrade.lineNumber];
+    const newValue = oldValue.replace(
+      /^.+?(\s.*)?$/,
+      `${upgrade.depName}${upgrade.newValue}$1`
+    );
+    lines[upgrade.lineNumber] = newValue;
     return lines.join('\n');
   } catch (err) {
     logger.info({ err }, 'Error setting new package version');
diff --git a/test/_fixtures/pip_requirements/requirements3.txt b/test/_fixtures/pip_requirements/requirements3.txt
new file mode 100644
index 0000000000..ae45a3a356
--- /dev/null
+++ b/test/_fixtures/pip_requirements/requirements3.txt
@@ -0,0 +1,5 @@
+Django==1 # renovate
+distribute==0.6.27 # renovate: ignore
+dj-database-url==0.2 # pyup: nothing
+psycopg2==2.4.5 # renovate:
+wsgiref==0.1.2 # something else
diff --git a/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap b/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap
index c366526d88..f352abc926 100644
--- a/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap
+++ b/test/manager/pip_requirements/__snapshots__/extract.spec.js.snap
@@ -74,3 +74,44 @@ Array [
   },
 ]
 `;
+
+exports[`lib/manager/pip_requirements/extract extractDependencies() handles comments and commands 1`] = `
+Array [
+  Object {
+    "currentValue": "==1",
+    "depName": "Django",
+    "lineNumber": 0,
+    "purl": "pkg:pypi/Django",
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==0.6.27",
+    "depName": "distribute",
+    "lineNumber": 1,
+    "purl": "pkg:pypi/distribute",
+    "skipReason": "ignored",
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==0.2",
+    "depName": "dj-database-url",
+    "lineNumber": 2,
+    "purl": "pkg:pypi/dj-database-url",
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==2.4.5",
+    "depName": "psycopg2",
+    "lineNumber": 3,
+    "purl": "pkg:pypi/psycopg2",
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==0.1.2",
+    "depName": "wsgiref",
+    "lineNumber": 4,
+    "purl": "pkg:pypi/wsgiref",
+    "versionScheme": "pep440",
+  },
+]
+`;
diff --git a/test/manager/pip_requirements/__snapshots__/update.spec.js.snap b/test/manager/pip_requirements/__snapshots__/update.spec.js.snap
new file mode 100644
index 0000000000..381e373e60
--- /dev/null
+++ b/test/manager/pip_requirements/__snapshots__/update.spec.js.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/pip_requirements/update updateDependency replaces existing value 1`] = `
+"--index-url http://example.com/private-pypi/
+# simple comment
+url==1.0.1
+some-other-package==1.0.0
+not_semver==1.9
+
+"
+`;
+
+exports[`manager/pip_requirements/update updateDependency replaces existing value with comment 1`] = `
+"Django==1 # renovate
+distribute==0.6.27 # renovate: ignore
+dj-database-url==0.2 # pyup: nothing
+psycopg2==2.4.6 # renovate:
+wsgiref==0.1.2 # something else
+"
+`;
diff --git a/test/manager/pip_requirements/extract.spec.js b/test/manager/pip_requirements/extract.spec.js
index 1f4a4b8384..9904e56da4 100644
--- a/test/manager/pip_requirements/extract.spec.js
+++ b/test/manager/pip_requirements/extract.spec.js
@@ -11,6 +11,10 @@ const requirements2 = fs.readFileSync(
   'test/_fixtures/pip_requirements/requirements2.txt',
   'utf8'
 );
+const requirements3 = fs.readFileSync(
+  'test/_fixtures/pip_requirements/requirements3.txt',
+  'utf8'
+);
 
 describe('lib/manager/pip_requirements/extract', () => {
   describe('extractDependencies()', () => {
@@ -31,5 +35,10 @@ describe('lib/manager/pip_requirements/extract', () => {
       expect(res).toMatchSnapshot();
       expect(res).toHaveLength(5);
     });
+    it('handles comments and commands', () => {
+      const res = extractDependencies(requirements3, config).deps;
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(5);
+    });
   });
 });
diff --git a/test/manager/pip_requirements/update.spec.js b/test/manager/pip_requirements/update.spec.js
index 086dd07b1a..3caa64fb2f 100644
--- a/test/manager/pip_requirements/update.spec.js
+++ b/test/manager/pip_requirements/update.spec.js
@@ -8,15 +8,21 @@ const requirements = fs.readFileSync(
   'utf8'
 );
 
+const requirements3 = fs.readFileSync(
+  'test/_fixtures/pip_requirements/requirements3.txt',
+  'utf8'
+);
+
 describe('manager/pip_requirements/update', () => {
   describe('updateDependency', () => {
     it('replaces existing value', () => {
       const upgrade = {
         depName: 'url',
         lineNumber: 2,
-        newValue: '1.0.1',
+        newValue: '==1.0.1',
       };
       const res = updateDependency(requirements, upgrade);
+      expect(res).toMatchSnapshot();
       expect(res).not.toEqual(requirements);
       expect(res.includes(upgrade.newValue)).toBe(true);
     });
@@ -24,5 +30,16 @@ describe('manager/pip_requirements/update', () => {
       const res = updateDependency(null, null);
       expect(res).toBe(null);
     });
+    it('replaces existing value with comment', () => {
+      const upgrade = {
+        depName: 'psycopg2',
+        lineNumber: 3,
+        newValue: '==2.4.6',
+      };
+      const res = updateDependency(requirements3, upgrade);
+      expect(res).toMatchSnapshot();
+      expect(res).not.toEqual(requirements3);
+      expect(res.includes(upgrade.newValue)).toBe(true);
+    });
   });
 });
-- 
GitLab