match_face/.venv/Lib/site-packages/pipenv/patched/safety/alerts/requirements.py

340 lines
11 KiB
Python
Raw Normal View History

from __future__ import unicode_literals
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet
import pipenv.patched.pip._vendor.requests as requests
from datetime import datetime
from pipenv.vendor.dparse import parse, parser, updater, filetypes
from pipenv.vendor.dparse.dependencies import Dependency
from pipenv.vendor.dparse.parser import setuptools_parse_requirements_backport as parse_requirements
class RequirementFile(object):
def __init__(self, path, content, sha=None):
self.path = path
self.content = content
self.sha = sha
self._requirements = None
self._other_files = None
self._is_valid = None
self.is_pipfile = False
self.is_pipfile_lock = False
self.is_setup_cfg = False
def __str__(self):
return "RequirementFile(path='{path}', sha='{sha}', content='{content}')".format(
path=self.path,
content=self.content[:30] + "[truncated]" if len(self.content) > 30 else self.content,
sha=self.sha
)
@property
def is_valid(self):
if self._is_valid is None:
self._parse()
return self._is_valid
@property
def requirements(self):
if not self._requirements:
self._parse()
return self._requirements
@property
def other_files(self):
if not self._other_files:
self._parse()
return self._other_files
@staticmethod
def parse_index_server(line):
return parser.Parser.parse_index_server(line)
def _hash_parser(self, line):
return parser.Parser.parse_hashes(line)
def _parse_requirements_txt(self):
self.parse_dependencies(filetypes.requirements_txt)
def _parse_conda_yml(self):
self.parse_dependencies(filetypes.conda_yml)
def _parse_tox_ini(self):
self.parse_dependencies(filetypes.tox_ini)
def _parse_pipfile(self):
self.parse_dependencies(filetypes.pipfile)
self.is_pipfile = True
def _parse_pipfile_lock(self):
self.parse_dependencies(filetypes.pipfile_lock)
self.is_pipfile_lock = True
def _parse_setup_cfg(self):
self.parse_dependencies(filetypes.setup_cfg)
self.is_setup_cfg = True
def _parse(self):
self._requirements, self._other_files = [], []
if self.path.endswith('.yml') or self.path.endswith(".yaml"):
self._parse_conda_yml()
elif self.path.endswith('.ini'):
self._parse_tox_ini()
elif self.path.endswith("Pipfile"):
self._parse_pipfile()
elif self.path.endswith("Pipfile.lock"):
self._parse_pipfile_lock()
elif self.path.endswith('setup.cfg'):
self._parse_setup_cfg()
else:
self._parse_requirements_txt()
self._is_valid = len(self._requirements) > 0 or len(self._other_files) > 0
def parse_dependencies(self, file_type):
result = parse(
self.content,
path=self.path,
sha=self.sha,
file_type=file_type,
marker=(
("pyup: ignore file", "pyup:ignore file"), # file marker
("pyup: ignore", "pyup:ignore"), # line marker
)
)
for dep in result.dependencies:
req = Requirement(
name=dep.name,
specs=dep.specs,
line=dep.line,
lineno=dep.line_numbers[0] if dep.line_numbers else 0,
extras=dep.extras,
file_type=file_type,
)
req.index_server = dep.index_server
if self.is_pipfile:
req.pipfile = self.path
req.hashes = dep.hashes
self._requirements.append(req)
self._other_files = result.resolved_files
def iter_lines(self, lineno=0):
for line in self.content.splitlines()[lineno:]:
yield line
@classmethod
def resolve_file(cls, file_path, line):
return parser.Parser.resolve_file(file_path, line)
class Requirement(object):
def __init__(self, name, specs, line, lineno, extras, file_type):
self.name = name
self.key = name.lower()
self.specs = specs
self.line = line
self.lineno = lineno
self.index_server = None
self.extras = extras
self.hashes = []
self.file_type = file_type
self.pipfile = None
self.hashCmp = (
self.key,
self.specs,
frozenset(self.extras),
)
self._is_insecure = None
self._changelog = None
if len(self.specs._specs) == 1 and next(iter(self.specs._specs))._spec[0] == "~=":
# convert compatible releases to something more easily consumed,
# e.g. '~=1.2.3' is equivalent to '>=1.2.3,<1.3.0', while '~=1.2'
# is equivalent to '>=1.2,<2.0'
min_version = next(iter(self.specs._specs))._spec[1]
max_version = list(parse_version(min_version).release)
max_version[-1] = 0
max_version[-2] = max_version[-2] + 1
max_version = '.'.join(str(x) for x in max_version)
self.specs = SpecifierSet('>=%s,<%s' % (min_version, max_version))
def __eq__(self, other):
return (
isinstance(other, Requirement) and
self.hashCmp == other.hashCmp
)
def __ne__(self, other):
return not self == other
def __str__(self):
return "Requirement.parse({line}, {lineno})".format(line=self.line, lineno=self.lineno)
def __repr__(self):
return self.__str__()
@property
def is_pinned(self):
if len(self.specs._specs) == 1 and next(iter(self.specs._specs))._spec[0] == "==":
return True
return False
@property
def is_open_ranged(self):
if len(self.specs._specs) == 1 and next(iter(self.specs._specs))._spec[0] == ">=":
return True
return False
@property
def is_ranged(self):
return len(self.specs._specs) >= 1 and not self.is_pinned
@property
def is_loose(self):
return len(self.specs._specs) == 0
@staticmethod
def convert_semver(version):
semver = {'major': 0, "minor": 0, "patch": 0}
version = version.split(".")
# don't be overly clever here. repitition makes it more readable and works exactly how
# it is supposed to
try:
semver['major'] = int(version[0])
semver['minor'] = int(version[1])
semver['patch'] = int(version[2])
except (IndexError, ValueError):
pass
return semver
@property
def can_update_semver(self):
# return early if there's no update filter set
if "pyup: update" not in self.line:
return True
update = self.line.split("pyup: update")[1].strip().split("#")[0]
current_version = Requirement.convert_semver(next(iter(self.specs._specs))._spec[1])
next_version = Requirement.convert_semver(self.latest_version)
if update == "major":
if current_version['major'] < next_version['major']:
return True
elif update == 'minor':
if current_version['major'] < next_version['major'] \
or current_version['minor'] < next_version['minor']:
return True
return False
@property
def filter(self):
rqfilter = False
if "rq.filter:" in self.line:
rqfilter = self.line.split("rq.filter:")[1].strip().split("#")[0]
elif "pyup:" in self.line:
if "pyup: update" not in self.line:
rqfilter = self.line.split("pyup:")[1].strip().split("#")[0]
# unset the filter once the date set in 'until' is reached
if "until" in rqfilter:
rqfilter, until = [l.strip() for l in rqfilter.split("until")]
try:
until = datetime.strptime(until, "%Y-%m-%d")
if until < datetime.now():
rqfilter = False
except ValueError:
# wrong date formatting
pass
if rqfilter:
try:
rqfilter, = parse_requirements("filter " + rqfilter)
if len(rqfilter.specifier._specs) > 0:
return rqfilter.specifier
except ValueError:
pass
return False
@property
def version(self):
if self.is_pinned:
return next(iter(self.specs._specs))._spec[1]
specs = self.specs
if self.filter:
specs = SpecifierSet(
",".join(["".join(s._spec) for s in list(specs._specs) + list(self.filter._specs)])
)
return self.get_latest_version_within_specs(
specs,
versions=self.package.versions,
prereleases=self.prereleases
)
def get_hashes(self, version):
r = requests.get('https://pypi.org/pypi/{name}/{version}/json'.format(
name=self.key,
version=version
))
hashes = []
data = r.json()
for item in data.get("urls", {}):
sha256 = item.get("digests", {}).get("sha256", False)
if sha256:
hashes.append({"hash": sha256, "method": "sha256"})
return hashes
def update_version(self, content, version, update_hashes=True):
if self.file_type == filetypes.tox_ini:
updater_class = updater.ToxINIUpdater
elif self.file_type == filetypes.conda_yml:
updater_class = updater.CondaYMLUpdater
elif self.file_type == filetypes.requirements_txt:
updater_class = updater.RequirementsTXTUpdater
elif self.file_type == filetypes.pipfile:
updater_class = updater.PipfileUpdater
elif self.file_type == filetypes.pipfile_lock:
updater_class = updater.PipfileLockUpdater
elif self.file_type == filetypes.setup_cfg:
updater_class = updater.SetupCFGUpdater
else:
raise NotImplementedError
dep = Dependency(
name=self.name,
specs=self.specs,
line=self.line,
line_numbers=[self.lineno, ] if self.lineno != 0 else None,
dependency_type=self.file_type,
hashes=self.hashes,
extras=self.extras
)
hashes = []
if self.hashes and update_hashes:
hashes = self.get_hashes(version)
return updater_class.update(
content=content,
dependency=dep,
version=version,
hashes=hashes,
spec="=="
)
@classmethod
def parse(cls, s, lineno, file_type=filetypes.requirements_txt):
# setuptools requires a space before the comment. If this isn't the case, add it.
if "\t#" in s:
parsed, = parse_requirements(s.replace("\t#", "\t #"))
else:
parsed, = parse_requirements(s)
return cls(
name=parsed.name,
specs=parsed.specifier,
line=s,
lineno=lineno,
extras=parsed.extras,
file_type=file_type
)