Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 097d52e08db8d75c2221dae3ec50ee8995b1218b @socketubs committed
Showing with 535 additions and 0 deletions.
  1. +2 −0 .coveragerc
  2. +12 −0 .gitignore
  3. +42 −0 README.md
  4. +1 −0 examples/requirements.py
  5. +1 −0 examples/requirements.txt
  6. +1 −0 examples/requirements/base.txt
  7. +3 −0 examples/requirements/tests.txt
  8. +228 −0 requirements.py
  9. +228 −0 tests/test_base.py
  10. +17 −0 tox.ini
2 .coveragerc
@@ -0,0 +1,2 @@
+[report]
+include = requirements.py
12 .gitignore
@@ -0,0 +1,12 @@
+dist
+build
+*.pyc
+*.egg-info
+__pycache__
+setup.py
+.tox
+htmlcov
+.cache
+.coverage
+examples/dist
+examples/build
42 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`.
1 examples/requirements.py
1 examples/requirements.txt
@@ -0,0 +1 @@
+-r requirements/base.txt
1 examples/requirements/base.txt
@@ -0,0 +1 @@
+requests>=2.9.1
3 examples/requirements/tests.txt
@@ -0,0 +1,3 @@
+-r base.txt
+
+flake8
228 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()
228 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']))
17 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

0 comments on commit 097d52e

Please sign in to comment.
Something went wrong with that request. Please try again.