From 097d52e08db8d75c2221dae3ec50ee8995b1218b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Geoffrey=20Leh=C3=A9e?= <glehee@oasiswork.fr>
Date: Mon, 21 Mar 2016 14:24:46 +0100
Subject: [PATCH] Initial commit

---
 .coveragerc                     |   2 +
 .gitignore                      |  12 +++
 README.md                       |  42 ++++++++
 examples/requirements.py        |   1 +
 examples/requirements.txt       |   1 +
 examples/requirements/base.txt  |   1 +
 examples/requirements/tests.txt |   3 +
 requirements.py                 | 228 ++++++++++++++++++++++++++++++++++++++++
 tests/test_base.py              | 228 ++++++++++++++++++++++++++++++++++++++++
 tox.ini                         |  17 +++
 10 files changed, 535 insertions(+)
 create mode 100644 .coveragerc
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 120000 examples/requirements.py
 create mode 100644 examples/requirements.txt
 create mode 100644 examples/requirements/base.txt
 create mode 100644 examples/requirements/tests.txt
 create mode 100644 requirements.py
 create mode 100644 tests/test_base.py
 create mode 100644 tox.ini

diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..066ee72
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,2 @@
+[report]
+include = requirements.py
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0082f89
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+dist
+build
+*.pyc
+*.egg-info
+__pycache__
+setup.py
+.tox
+htmlcov
+.cache
+.coverage
+examples/dist
+examples/build
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c48ac9d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+# Requirements
+
+*☛ Python requirements for Humans ™*
+
+Write your adorable `requirements.txt` once and forget `setup.py` hassles.
+
+```python
+from setuptools import setup
+from requirements import r
+
+setup(
+    name='your-package',
+    version='0.0.1',
+    **r.requirements)
+```
+
+### Features
+
+* Requirements discovery
+* Manage `dependency_links` and `tests_require`
+* Just drop `requirements.py` in your package directory
+* Works well with [pip-tools](https://github.com/nvie/pip-tools)
+* Configurable for different requirements layout
+* Python `2.7`, `3.3`, `3.4`, `3.5`
+* Very light, well tested, no dependencies and more!
+
+
+### Usage
+
+* Download latest `requirements.py` release in your package root directory
+* Import it in your `setup.py`, like in previous example
+
+Some variables are configurable like that:
+
+```python
+from requirements import r
+
+r.requirements_path = 'reqs.txt'
+r.tests_requirements_path = 'reqs-tests.txt'
+```
+
+License is `MIT`.
diff --git a/examples/requirements.py b/examples/requirements.py
new file mode 120000
index 0000000..81386d7
--- /dev/null
+++ b/examples/requirements.py
@@ -0,0 +1 @@
+../requirements.py
\ No newline at end of file
diff --git a/examples/requirements.txt b/examples/requirements.txt
new file mode 100644
index 0000000..5603c37
--- /dev/null
+++ b/examples/requirements.txt
@@ -0,0 +1 @@
+-r requirements/base.txt
diff --git a/examples/requirements/base.txt b/examples/requirements/base.txt
new file mode 100644
index 0000000..28fbc91
--- /dev/null
+++ b/examples/requirements/base.txt
@@ -0,0 +1 @@
+requests>=2.9.1
diff --git a/examples/requirements/tests.txt b/examples/requirements/tests.txt
new file mode 100644
index 0000000..57f3324
--- /dev/null
+++ b/examples/requirements/tests.txt
@@ -0,0 +1,3 @@
+-r base.txt
+
+flake8
diff --git a/requirements.py b/requirements.py
new file mode 100644
index 0000000..f17bcbe
--- /dev/null
+++ b/requirements.py
@@ -0,0 +1,228 @@
+import os
+import re
+import logging
+import warnings
+from pkg_resources import Requirement as Req
+
+try:
+    from urllib.parse import urlparse
+except ImportError:
+    from urlparse import urlparse
+
+
+__version__ = '0.1.0'
+
+logging.basicConfig(level=logging.WARNING)
+
+VCS = ['git', 'hg', 'svn', 'bzr']
+
+
+class Requirement(object):
+    """
+        This class is inspired from
+        https://github.com/davidfischer/requirements-parser/blob/master/requirements/requirement.py#L30
+        License: BSD
+    """
+    def __init__(self, line):
+        self.line = line
+        self.is_editable = False
+        self.is_local_file = False
+        self.is_specifier = False
+        self.vcs = None
+        self.name = None
+        self.uri = None
+        self.full_uri = None
+        self.path = None
+        self.revision = None
+        self.scheme = None
+        self.login = None
+        self.extras = []
+        self.specs = []
+
+    def __repr__(self):
+        return '<Requirement: "{0}">'.format(self.line)
+
+    @classmethod
+    def parse(cls, line, editable=False):
+        """
+        Parses a Requirement from an "editable" requirement which is either
+        a local project path or a VCS project URI.
+
+        See: pip/req.py:from_editable()
+
+        :param line: an "editable" requirement
+        :returns: a Requirement instance for the given line
+        :raises: ValueError on an invalid requirement
+        """
+        if editable:
+            req = cls('-e {0}'.format(line))
+            req.is_editable = True
+        else:
+            req = cls(line)
+
+        url = urlparse(line)
+        req.uri = None
+        if url.scheme:
+            req.scheme = url.scheme
+            req.uri = url.scheme + '://' + url.netloc + url.path
+            fragment = url.fragment.split(' ')[0].strip()
+            req.name = fragment.split('egg=')[-1] or None
+            req.path = url.path
+            if fragment:
+                req.uri += '#{}'.format(fragment)
+            if url.username or url.password:
+                username = url.username or ''
+                password = url.password or ''
+                req.login = username + ':' + password
+            if '@' in url.path:
+                req.revision = url.path.split('@')[-1]
+
+            for vcs in VCS:
+                if req.uri.startswith(vcs):
+                    req.vcs = vcs
+            if req.scheme.startswith('file://'):
+                req.is_local_file = True
+
+        if not req.vcs and not req.is_local_file and 'egg=' not in line:
+            # This is a requirement specifier.
+            # Delegate to pkg_resources and hope for the best
+            req.is_specifier = True
+            pkg_req = Req.parse(line)
+            req.name = pkg_req.unsafe_name
+            req.extras = list(pkg_req.extras)
+            req.specs = pkg_req.specs
+            if req.specs:
+                req.specs = sorted(req.specs)
+
+        return req
+
+
+class Requirements:
+    def __init__(
+            self,
+            requirements="requirements.txt",
+            tests_requirements="requirements/tests.txt"):
+        self.requirements_path = requirements
+        self.tests_requirements_path = tests_requirements
+
+    def format_specifiers(self, requirement):
+        return ', '.join(
+            ['{} {}'.format(s[0], s[1]) for s in requirement.specs])
+
+    @property
+    def install_requires(self):
+        dependencies = []
+        for requirement in self.parse(self.requirements_path):
+            if not requirement.is_editable and not requirement.uri \
+                    and not requirement.vcs:
+                full_name = requirement.name
+                specifiers = self.format_specifiers(requirement)
+                if specifiers:
+                    full_name = "{} {}".format(full_name, specifiers)
+                dependencies.append(full_name)
+        for requirement in self.get_dependency_links():
+            print(":: (base:install_requires) {}".format(requirement.name))
+            dependencies.append(requirement.name)
+        return dependencies
+
+    @property
+    def tests_require(self):
+        dependencies = []
+        for requirement in self.parse(self.tests_requirements_path):
+            if not requirement.is_editable and not requirement.uri \
+                    and not requirement.vcs:
+                full_name = requirement.name
+                specifiers = self.format_specifiers(requirement)
+                if specifiers:
+                    full_name = "{} {}".format(full_name, specifiers)
+                print(":: (tests:tests_require) {}".format(full_name))
+                dependencies.append(full_name)
+        return dependencies
+
+    @property
+    def dependency_links(self):
+        dependencies = []
+        for requirement in self.parse(self.requirements_path):
+            if requirement.uri or requirement.vcs or requirement.path:
+                print(":: (base:dependency_links) {}".format(
+                    requirement.uri))
+                dependencies.append(requirement.uri)
+        return dependencies
+
+    @property
+    def dependencies(self):
+        install_requires = self.install_requires
+        dependency_links = self.dependency_links
+        tests_require = self.tests_require
+        if dependency_links:
+            print(
+                "\n"
+                "!! Some dependencies are linked to repository or local path.")
+            print(
+                "!! You'll need to run pip with following option: "
+                "`--process-dependency-links`"
+                "\n")
+        return {
+            'install_requires': install_requires,
+            'dependency_links': dependency_links,
+            'tests_require': tests_require}
+
+    def get_dependency_links(self):
+        dependencies = []
+        for requirement in self.parse(self.requirements_path):
+            if requirement.uri or requirement.vcs or requirement.path:
+                dependencies.append(requirement)
+        return dependencies
+
+    def parse(self, path=None):
+        path = path or self.requirements_path
+        path = os.path.abspath(path)
+        base_directory = os.path.dirname(path)
+
+        if not os.path.exists(path):
+            warnings.warn(
+                'Requirements file: {} does not exists.'.format(path))
+            return
+
+        with open(path) as requirements:
+            for index, line in enumerate(requirements.readlines()):
+                index += 1
+                line = line.strip()
+                if not line:
+                    logging.debug('Empty line (line {} from {})'.format(
+                        index, path))
+                    continue
+                elif line.startswith('#'):
+                    logging.debug(
+                        'Comments line (line {} from {})'.format(index, path))
+                elif line.startswith('-f') or \
+                        line.startswith('--find-links') or \
+                        line.startswith('-i') or \
+                        line.startswith('--index-url') or \
+                        line.startswith('--extra-index-url') or \
+                        line.startswith('--no-index'):
+                    warnings.warn('Private repos not supported. Skipping.')
+                    continue
+                elif line.startswith('-Z') or line.startswith(
+                        '--always-unzip'):
+                    warnings.warn('Unused option --always-unzip. Skipping.')
+                    continue
+                elif line.startswith('-r') or line.startswith('--requirement'):
+                    logging.debug(
+                        'Pining to another requirements file '
+                        '(line {} from {})'.format(index, path))
+                    for _line in self.parse(path=os.path.join(
+                            base_directory, line.split()[1])):
+                        yield _line
+                elif line.startswith('-e') or line.startswith('--editable'):
+                    # Editable installs are either a local project path
+                    # or a VCS project URI
+                    yield Requirement.parse(
+                        re.sub(r'^(-e|--editable=?)\s*', '', line),
+                        editable=True)
+                else:
+                    logging.debug('Found "{}" (line {} from {})'.format(
+                        line, index, path))
+                    yield Requirement.parse(line, editable=False)
+
+r = Requirements()
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..c709ac6
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,228 @@
+import os
+import sys
+import shutil
+import tempfile
+from unittest import TestCase
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from requirements import Requirement
+from requirements import Requirements
+
+ORIGINAL_DIRECTORY = os.getcwd()
+
+
+class RequirementsTestCase(TestCase):
+    def setUp(self):
+        self.root_directory = tempfile.mkdtemp()
+        os.chdir(self.root_directory)
+
+        self.r = Requirements()
+
+    def tearDown(self):
+        os.chdir(ORIGINAL_DIRECTORY)
+        shutil.rmtree(self.root_directory)
+
+    def test_requirement_repr(self):
+        r = Requirement.parse('requests==2.9.1')
+        self.assertEqual(r.__repr__(), '<Requirement: "requests==2.9.1">')
+
+    def test_requirement_parsing(self):
+        line = '  requests==2.9.1,>=2.8.1 # jambon'
+        r = Requirement.parse(line)
+        self.assertEqual(r.line, line)
+        self.assertEqual(r.name, 'requests')
+        self.assertEqual(r.specs, [('==', '2.9.1'), ('>=', '2.8.1')])
+
+    def test_detect_files(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('requests==2.9.1\n')
+
+        os.mkdir(os.path.join(self.root_directory, 'requirements'))
+        tests_requirements_path = os.path.join(
+            self.root_directory, 'requirements', 'tests.txt')
+
+        with open(tests_requirements_path, 'w') as f:
+            f.write('flake8==2.5.4\n')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(dependencies['tests_require'], ['flake8 == 2.5.4'])
+        self.assertEqual(
+            dependencies['install_requires'], ['requests == 2.9.1'])
+        self.assertEqual(dependencies['dependency_links'], [])
+
+    def test_different_paths(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'foo.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('requests==2.9.1\n')
+
+        os.mkdir(os.path.join(self.root_directory, 'moar-requirements'))
+        tests_requirements_path = os.path.join(
+            self.root_directory, 'moar-requirements', 'bar.txt')
+
+        with open(tests_requirements_path, 'w') as f:
+            f.write('flake8==2.5.4\n')
+
+        self.r.requirements_path = requirements_path
+        self.r.tests_requirements_path = tests_requirements_path
+
+        dependencies = self.r.dependencies
+        self.assertEqual(dependencies['tests_require'], ['flake8 == 2.5.4'])
+        self.assertEqual(
+            dependencies['install_requires'], ['requests == 2.9.1'])
+        self.assertEqual(dependencies['dependency_links'], [])
+
+    def test_empty_lines(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('\n\n')
+            f.write('requests==2.9.1 #I like ham\n')
+            f.write('\n')
+            f.write('boto')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(
+            sorted(dependencies['install_requires']),
+            sorted(['requests == 2.9.1', 'boto']))
+
+    def test_comments_line_ignored(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('# boto==python3-lol\n')
+            f.write('requests==2.9.1 #I like ham\n')
+
+        os.mkdir(os.path.join(self.root_directory, 'requirements'))
+        tests_requirements_path = os.path.join(
+            self.root_directory, 'requirements', 'tests.txt')
+
+        with open(tests_requirements_path, 'w') as f:
+            f.write('flake8==2.5.4\n')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(dependencies['tests_require'], ['flake8 == 2.5.4'])
+        self.assertEqual(dependencies['install_requires'], ['requests == 2.9.1'])
+        self.assertEqual(dependencies['dependency_links'], [])
+
+    def test_multi_specifiers(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+        tests_requirements_path = os.path.join(
+            self.root_directory, 'tests-requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('requests<=2.9.1,>=2.8.5')
+
+        with open(tests_requirements_path, 'w') as f:
+            f.write('requests   <=  2.9.1 , >=2.8.5')
+
+        self.r.tests_requirements_path = 'tests-requirements.txt'
+
+        dependencies = self.r.dependencies
+        self.assertEqual(
+            dependencies['install_requires'], ['requests <= 2.9.1, >= 2.8.5'])
+        self.assertEqual(
+            dependencies['tests_require'], ['requests <= 2.9.1, >= 2.8.5'])
+
+    def test_ignore_every_private_links(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('--no-index --find-links=/tmp/wheelhouse SomePackage\n')
+            f.write('--find-links=/tmp/wheelhouse SomePackage\n')
+            f.write('-f /tmp/wheelhouse SomePackage\n')
+            f.write('--extra-index-url http://foo.bar SomePackage\n')
+            f.write('-i http://foo.bar SomePackage\n')
+            f.write('requests\n')
+            f.write('--index-url http://foo.bar SomePackage\n')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(dependencies['install_requires'], ['requests'])
+
+    def test_ignore_arguments(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('requests\n')
+            f.write('--always-unzip SomePackage\n')
+            f.write('-Z SomePackage\n')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(dependencies['install_requires'], ['requests'])
+
+    def test_requirements_inception(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+        requirements_path_02 = os.path.join(
+            self.root_directory, 'requirements-02.txt')
+        requirements_path_03 = os.path.join(
+            self.root_directory, 'requirements', 'requirements-03.txt')
+
+        os.mkdir(os.path.join(self.root_directory, 'requirements'))
+
+        with open(requirements_path, 'w') as f:
+            f.write('requests\n')
+            f.write('-r requirements-02.txt\n')
+
+        with open(requirements_path_02, 'w') as f:
+            f.write('boto\n')
+            f.write('--requirement requirements/requirements-03.txt\n')
+
+        with open(requirements_path_03, 'w') as f:
+            f.write('isit==0.1.0\n')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(
+            sorted(dependencies['install_requires']),
+            sorted(['requests', 'boto', 'isit == 0.1.0']))
+
+    def test_dependency_links(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('boto\n')
+            f.write('-e git+https://github.com/kennethreitz/requests.git@master#egg=requests\n')
+            f.write('-e svn+http://foo:bar@svn.myproject.org/svn/MyProject/trunk@2019#egg=foo01    # COMMENT\n')
+            f.write('-e git+ssh://git@myproject.org/MyProject/#egg=foo02\n')
+            f.write('-e hg+http://hg.myproject.org/MyProject/@da39a3ee5e6b#egg=foo03\n')
+            f.write('-e bzr+https://bzr.myproject.org/MyProject/trunk/@2019#egg=foo04\n')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(
+            sorted(dependencies['install_requires']),
+            sorted(['requests', 'boto', 'foo01', 'foo02', 'foo03', 'foo04']))
+        self.assertEqual(
+            sorted(dependencies['dependency_links']),
+            sorted([
+                'git+https://github.com/kennethreitz/requests.git@master#egg=requests',
+                'svn+http://foo:bar@svn.myproject.org/svn/MyProject/trunk@2019#egg=foo01',
+                'git+ssh://git@myproject.org/MyProject/#egg=foo02',
+                'hg+http://hg.myproject.org/MyProject/@da39a3ee5e6b#egg=foo03',
+                'bzr+https://bzr.myproject.org/MyProject/trunk/@2019#egg=foo04']))
+
+    def test_http_link(self):
+        requirements_path = os.path.join(
+            self.root_directory, 'requirements.txt')
+
+        with open(requirements_path, 'w') as f:
+            f.write('boto\n')
+            f.write('http://someserver.org/packages/MyPackage-3.0.tar.gz#egg=foo\n')
+
+        dependencies = self.r.dependencies
+        self.assertEqual(
+            sorted(dependencies['install_requires']), sorted(['boto', 'foo']))
+        self.assertEqual(
+            sorted(dependencies['dependency_links']),
+            sorted([
+                'http://someserver.org/packages/MyPackage-3.0.tar.gz#egg=foo']))
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..67edacb
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,17 @@
+[tox]
+envlist = py27,py33,py34,py35
+skipsdist = True
+
+[testenv]
+deps =
+    pytest
+    flake8
+commands =
+    py.test tests
+    flake8 requirements.py
+
+[flake8]
+exclude = tests/*
+
+[pytest]
+testpaths = tests
