From 9a32f35ddcdb342dade5acea94b52a6e420df815 Mon Sep 17 00:00:00 2001
From: praveshtora <pravesh.tora@gmail.com>
Date: Mon, 22 Jul 2019 10:50:53 +0530
Subject: [PATCH] fix(pip_setup): handle updating when multiple deps per line
 (#4119)

---
 lib/manager/pip_requirements/update.js        |  24 ++-
 .../__snapshots__/update.spec.js.snap         | 177 ++++++++++++++++++
 test/manager/pip_requirements/update.spec.js  |  35 ++++
 test/manager/pip_setup/_fixtures/setup-2.py   |  81 ++++++++
 4 files changed, 314 insertions(+), 3 deletions(-)
 create mode 100644 test/manager/pip_setup/_fixtures/setup-2.py

diff --git a/lib/manager/pip_requirements/update.js b/lib/manager/pip_requirements/update.js
index 0c8783352f..ba1ad061cb 100644
--- a/lib/manager/pip_requirements/update.js
+++ b/lib/manager/pip_requirements/update.js
@@ -10,10 +10,28 @@ function updateDependency(fileContent, upgrade) {
     logger.debug(`pip_requirements.updateDependency(): ${upgrade.newValue}`);
     const lines = fileContent.split('\n');
     const oldValue = lines[upgrade.lineNumber];
-    const newValue = oldValue.replace(
-      new RegExp(dependencyPattern),
-      `$1$2${upgrade.newValue}`
+    let newValue;
+    const multiDependencyRegex = new RegExp(
+      `(install_requires\\s*[=]\\s*\\[.*)(${upgrade.depName}.+?(?='))(.*])`,
+      'g'
     );
+    const multipleDependencyMatch = multiDependencyRegex.exec(oldValue);
+    if (multipleDependencyMatch) {
+      const dependency = multipleDependencyMatch[2];
+      const updatedDependency = dependency.replace(
+        new RegExp(dependencyPattern),
+        `$1$2${upgrade.newValue}`
+      );
+      newValue = oldValue.replace(
+        multiDependencyRegex,
+        `$1${updatedDependency}$3`
+      );
+    } else {
+      newValue = oldValue.replace(
+        new RegExp(dependencyPattern),
+        `$1$2${upgrade.newValue}`
+      );
+    }
     lines[upgrade.lineNumber] = newValue;
     return lines.join('\n');
   } catch (err) {
diff --git a/test/manager/pip_requirements/__snapshots__/update.spec.js.snap b/test/manager/pip_requirements/__snapshots__/update.spec.js.snap
index 5d21201e80..be49d8f76c 100644
--- a/test/manager/pip_requirements/__snapshots__/update.spec.js.snap
+++ b/test/manager/pip_requirements/__snapshots__/update.spec.js.snap
@@ -1,5 +1,182 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`manager/pip_requirements/update updateDependency handles dependencies in different lines in setup.py 1`] = `
+"
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup
+
+setup(
+    author='Simon Davy',
+    author_email='simon.davy@canonical.com',
+    classifiers=[
+        'License :: OSI Approved :: Apache Software License',
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'Natural Language :: English',
+        'Topic :: Internet :: WWW/HTTP :: WSGI',
+        'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware',
+        'Topic :: System :: Logging',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: Implementation :: CPython',
+    ],
+    description='A common WSGI stack',
+    entry_points=dict(
+        console_scripts=[
+            'talisker=talisker:run_gunicorn',
+            'talisker.run=talisker:run',
+            'talisker.gunicorn=talisker:run_gunicorn',
+            'talisker.gunicorn.eventlet=talisker:run_gunicorn_eventlet',
+            'talisker.gunicorn.gevent=talisker:run_gunicorn_gevent',
+            'talisker.celery=talisker:run_celery',
+        ],
+    ),
+    extras_require=dict(
+        celery=[
+            'celery>=3.1.13.0,<5.0',
+        ],
+        dev=[
+            'logging_tree>=1.7',
+            'pygments>=2.2',
+            'psutil>=5.0',
+            'objgraph>=3.0',
+        ],
+        django=[
+            'django>=1.10,<2.0',
+        ],
+        flask=[
+            'flask>=0.11,<2.0',
+            'blinker>=1.4,<2.0',
+        ],
+        pg=[
+            'sqlparse',
+            'psycopg2',
+        ],
+        prometheus=[
+            'prometheus-client>=0.2.0,<0.5.0' + ',!=0.4.0,!=0.4.1',
+        ],
+    ),
+    include_package_data=True,
+    install_requires=[
+        'gunicorn>=19.7.0,<20.0',
+        'Werkzeug>=0.11.5,<0.15', 'statsd>=3.2.1,<4.0',
+        'requests>=2.10.0,<3.0', # renovate: ignore
+        'raven>=2.11.0', # pyup: nothing
+        'future>=0.15.2,<0.17',
+        'ipaddress>=1.0.16,<2.0;python_version<\\"3.3\\"',
+    ],
+    keywords=[
+        'talisker',
+    ],
+    name='talisker',
+    package_data=dict(
+        talisker=[
+            'logstash/*',
+        ],
+    ),
+    package_dir=dict(
+        talisker='talisker',
+    ),
+    packages=[
+        'talisker',
+    ],
+    test_suite='tests',
+    url='https://github.com/canonical-ols/talisker',
+    version='0.9.16',
+    zip_safe=False,
+)
+"
+`;
+
+exports[`manager/pip_requirements/update updateDependency handles multiple dependencies in same lines in setup.py 1`] = `
+"
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup
+
+setup(
+    author='Simon Davy',
+    author_email='simon.davy@canonical.com',
+    classifiers=[
+        'License :: OSI Approved :: Apache Software License',
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'Natural Language :: English',
+        'Topic :: Internet :: WWW/HTTP :: WSGI',
+        'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware',
+        'Topic :: System :: Logging',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: Implementation :: CPython',
+    ],
+    description='A common WSGI stack',
+    entry_points=dict(
+        console_scripts=[
+            'talisker=talisker:run_gunicorn',
+            'talisker.run=talisker:run',
+            'talisker.gunicorn=talisker:run_gunicorn',
+            'talisker.gunicorn.eventlet=talisker:run_gunicorn_eventlet',
+            'talisker.gunicorn.gevent=talisker:run_gunicorn_gevent',
+            'talisker.celery=talisker:run_celery',
+        ],
+    ),
+    extras_require=dict(
+        celery=[
+            'celery>=3.1.13.0,<5.0',
+        ],
+        dev=[
+            'logging_tree>=1.7',
+            'pygments>=2.2',
+            'psutil>=5.0',
+            'objgraph>=3.0',
+        ],
+        django=[
+            'django>=1.10,<2.0',
+        ],
+        flask=[
+            'flask>=0.11,<2.0',
+            'blinker>=1.4,<2.0',
+        ],
+        pg=[
+            'sqlparse',
+            'psycopg2',
+        ],
+        prometheus=[
+            'prometheus-client>=0.2.0,<0.5.0' + ',!=0.4.0,!=0.4.1',
+        ],
+    ),
+    include_package_data=True,
+    install_requires=['gunicorn>=19.7.0,<20.0', 'Werkzeug>=0.11.5,<0.15', 'pycryptodome==3.8.0','statsd>=3.2.1,<4.0', 'requests>=2.10.0,<3.0', 'raven>=5.27.1,<7.0','future>=0.15.2,<0.17',],
+    keywords=[
+        'talisker',
+    ],
+    name='talisker',
+    package_data=dict(
+        talisker=[
+            'logstash/*',
+        ],
+    ),
+    package_dir=dict(
+        talisker='talisker',
+    ),
+    packages=[
+        'talisker',
+    ],
+    test_suite='tests',
+    url='https://github.com/canonical-ols/talisker',
+    version='0.9.16',
+    zip_safe=False,
+)
+"
+`;
+
 exports[`manager/pip_requirements/update updateDependency replaces existing value 1`] = `
 "--index-url http://example.com/private-pypi/
 # simple comment
diff --git a/test/manager/pip_requirements/update.spec.js b/test/manager/pip_requirements/update.spec.js
index d25da17244..8a25d15ed5 100644
--- a/test/manager/pip_requirements/update.spec.js
+++ b/test/manager/pip_requirements/update.spec.js
@@ -18,6 +18,16 @@ const requirements4 = fs.readFileSync(
   'utf8'
 );
 
+const setupPy1 = fs.readFileSync(
+  'test/manager/pip_setup/_fixtures/setup.py',
+  'utf-8'
+);
+
+const setupPy2 = fs.readFileSync(
+  'test/manager/pip_setup/_fixtures/setup-2.py',
+  'utf-8'
+);
+
 describe('manager/pip_requirements/update', () => {
   describe('updateDependency', () => {
     it('replaces existing value', () => {
@@ -58,5 +68,30 @@ describe('manager/pip_requirements/update', () => {
       expect(res).not.toEqual(requirements4);
       expect(res.includes(upgrade.newValue)).toBe(true);
     });
+    it('handles dependencies in different lines in setup.py', () => {
+      const upgrade = {
+        depName: 'requests',
+        lineNumber: 64,
+        newValue: '>=2.11.0',
+      };
+      const res = updateDependency(setupPy1, upgrade);
+      expect(res).toMatchSnapshot();
+      expect(res).not.toEqual(setupPy1);
+      expect(res.includes(upgrade.newValue)).toBe(true);
+    });
+    it('handles multiple dependencies in same lines in setup.py', () => {
+      const upgrade = {
+        depName: 'pycryptodome',
+        lineNumber: 60,
+        newValue: '==3.8.0',
+      };
+      const res = updateDependency(setupPy2, upgrade);
+      expect(res).toMatchSnapshot();
+      expect(res).not.toEqual(setupPy2);
+      const expectedUpdate =
+        "install_requires=['gunicorn>=19.7.0,<20.0', 'Werkzeug>=0.11.5,<0.15', 'pycryptodome==3.8.0','statsd>=3.2.1,<4.0', 'requests>=2.10.0,<3.0', 'raven>=5.27.1,<7.0','future>=0.15.2,<0.17',],";
+      expect(res).toContain(expectedUpdate);
+      expect(res.includes(upgrade.newValue)).toBe(true);
+    });
   });
 });
diff --git a/test/manager/pip_setup/_fixtures/setup-2.py b/test/manager/pip_setup/_fixtures/setup-2.py
new file mode 100644
index 0000000000..de35905d41
--- /dev/null
+++ b/test/manager/pip_setup/_fixtures/setup-2.py
@@ -0,0 +1,81 @@
+
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup
+
+setup(
+    author='Simon Davy',
+    author_email='simon.davy@canonical.com',
+    classifiers=[
+        'License :: OSI Approved :: Apache Software License',
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'Natural Language :: English',
+        'Topic :: Internet :: WWW/HTTP :: WSGI',
+        'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware',
+        'Topic :: System :: Logging',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: Implementation :: CPython',
+    ],
+    description='A common WSGI stack',
+    entry_points=dict(
+        console_scripts=[
+            'talisker=talisker:run_gunicorn',
+            'talisker.run=talisker:run',
+            'talisker.gunicorn=talisker:run_gunicorn',
+            'talisker.gunicorn.eventlet=talisker:run_gunicorn_eventlet',
+            'talisker.gunicorn.gevent=talisker:run_gunicorn_gevent',
+            'talisker.celery=talisker:run_celery',
+        ],
+    ),
+    extras_require=dict(
+        celery=[
+            'celery>=3.1.13.0,<5.0',
+        ],
+        dev=[
+            'logging_tree>=1.7',
+            'pygments>=2.2',
+            'psutil>=5.0',
+            'objgraph>=3.0',
+        ],
+        django=[
+            'django>=1.10,<2.0',
+        ],
+        flask=[
+            'flask>=0.11,<2.0',
+            'blinker>=1.4,<2.0',
+        ],
+        pg=[
+            'sqlparse',
+            'psycopg2',
+        ],
+        prometheus=[
+            'prometheus-client>=0.2.0,<0.5.0' + ',!=0.4.0,!=0.4.1',
+        ],
+    ),
+    include_package_data=True,
+    install_requires=['gunicorn>=19.7.0,<20.0', 'Werkzeug>=0.11.5,<0.15', 'pycryptodome==3.7.3','statsd>=3.2.1,<4.0', 'requests>=2.10.0,<3.0', 'raven>=5.27.1,<7.0','future>=0.15.2,<0.17',],
+    keywords=[
+        'talisker',
+    ],
+    name='talisker',
+    package_data=dict(
+        talisker=[
+            'logstash/*',
+        ],
+    ),
+    package_dir=dict(
+        talisker='talisker',
+    ),
+    packages=[
+        'talisker',
+    ],
+    test_suite='tests',
+    url='https://github.com/canonical-ols/talisker',
+    version='0.9.16',
+    zip_safe=False,
+)
-- 
GitLab