Permalink
| 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() |