mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 22:50:18 +00:00
* Vendor new safety (#5217) * Closest to correct vendoring of ruamel. * Fix lint * New pipenv check with minimal output. * Remove decode_for_output use in pipenv check functions. * Use click.secho where is possible. * Rerun vendoring to get the latest safety version. * skip this test for now. Co-authored-by: Yeison Vargas <yeisonvargasjf@gmail.com>
This commit is contained in:
@@ -19,6 +19,7 @@ pytest-cov = "==3.*"
|
||||
typing-extensions = "==4.*"
|
||||
waitress = {version = "*", markers="sys_platform == 'win32'"}
|
||||
gunicorn = {version = "*", markers="sys_platform == 'linux'"}
|
||||
parse = "*"
|
||||
|
||||
[packages]
|
||||
|
||||
|
||||
Generated
+38
-29
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "732f0dc59c2417ea9c448ef70f1d642487fa85ca8a743caa583076d0a6ccf32d"
|
||||
"sha256": "ded711ae5f1cf2ba1a91d1ae79cc70609bfc9cf9b46789d6521246a5f540c57b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -24,10 +24,10 @@
|
||||
},
|
||||
"arpeggio": {
|
||||
"hashes": [
|
||||
"sha256:bfe349f252f82f82d84cb886f1d5081d1a31451e6045275e9f90b65d0daa06f1",
|
||||
"sha256:fed68a1cb7f529cbd4d725597cc811b7506885fcdef17d4cdcf564341a1e210b"
|
||||
"sha256:448e332deb0e9ccd04046f1c6c14529d197f41bc2fdb3931e43fc209042fbdd3",
|
||||
"sha256:d6b03839019bb8a68785f9292ee6a36b1954eb84b925b84a6b8a5e1e26d3ed3d"
|
||||
],
|
||||
"version": "==1.10.2"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
@@ -217,11 +217,11 @@
|
||||
},
|
||||
"exceptiongroup": {
|
||||
"hashes": [
|
||||
"sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a",
|
||||
"sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"
|
||||
"sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828",
|
||||
"sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==1.0.1"
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
"execnet": {
|
||||
"hashes": [
|
||||
@@ -268,16 +268,17 @@
|
||||
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
|
||||
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'linux'",
|
||||
"version": "==20.1.0"
|
||||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440",
|
||||
"sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"
|
||||
"sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f",
|
||||
"sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.5.8"
|
||||
"version": "==2.5.9"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
@@ -424,21 +425,28 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"parse": {
|
||||
"hashes": [
|
||||
"sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.19.0"
|
||||
},
|
||||
"parver": {
|
||||
"hashes": [
|
||||
"sha256:41a548c51b006a2f2522b54293cbfd2514bffa10774ece8430c9964a20cbd8b4",
|
||||
"sha256:c902e0653bcce927cc156a7fd9b3a51924cbce3bf3d0bfd49fc282bfd0c5dfd3"
|
||||
"sha256:c66d3347a4858643875ef959d8ba7a269d5964bfb690b0dd998b8f39da930be2",
|
||||
"sha256:d4a3dbb93c53373ee9a0ba055e4858c44169b204b912e49d003ead95db9a9bca"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.3.1"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.4"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93",
|
||||
"sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"
|
||||
"sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5",
|
||||
"sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.10.1"
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"pipenv": {
|
||||
"editable": true,
|
||||
@@ -450,11 +458,11 @@
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788",
|
||||
"sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"
|
||||
"sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7",
|
||||
"sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.5.2"
|
||||
"version": "==2.5.4"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
@@ -637,11 +645,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31",
|
||||
"sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"
|
||||
"sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840",
|
||||
"sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==65.5.1"
|
||||
"version": "==65.6.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -732,16 +740,17 @@
|
||||
},
|
||||
"sphinxcontrib-spelling": {
|
||||
"hashes": [
|
||||
"sha256:a129d5dd0c00c9d414bcdf599afddb55bae01f1d543636e7ebb09491ba2babd4",
|
||||
"sha256:a7ca90eea630c825657e1344000a11d7d7f547a865e0eebbed08d112beb073d1"
|
||||
"sha256:56561c3f6a155b0946914e4de988729859315729dc181b5e4dc8a68fe78de35a",
|
||||
"sha256:95a0defef8ffec6526f9e83b20cc24b08c9179298729d87976891840e3aa3064"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.6.2"
|
||||
"version": "==7.7.0"
|
||||
},
|
||||
"stdeb": {
|
||||
"hashes": [
|
||||
"sha256:08c22c9c03b28a140fe3ec5064b53a5288279f22e596ca06b0be698d50c93cf2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "sys_platform == 'linux'",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
@@ -787,11 +796,11 @@
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108",
|
||||
"sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"
|
||||
"sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e",
|
||||
"sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==20.16.6"
|
||||
"version": "==20.16.7"
|
||||
},
|
||||
"virtualenv-clone": {
|
||||
"hashes": [
|
||||
|
||||
+35
-3
@@ -440,7 +440,7 @@ def run(state, command, args):
|
||||
"--db",
|
||||
nargs=1,
|
||||
default=lambda: os.environ.get("PIPENV_SAFETY_DB"),
|
||||
help="Path to a local PyUp Safety vulnerabilities database."
|
||||
help="Path or URL to a PyUp Safety vulnerabilities database."
|
||||
" Default: ENV PIPENV_SAFETY_DB or None.",
|
||||
)
|
||||
@option(
|
||||
@@ -451,7 +451,7 @@ def run(state, command, args):
|
||||
)
|
||||
@option(
|
||||
"--output",
|
||||
type=Choice(["default", "json", "full-report", "bare"]),
|
||||
type=Choice(["default", "json", "full-report", "bare", "screen", "text", "minimal"]),
|
||||
default="default",
|
||||
help="Translates to --json, --full-report or --bare from PyUp Safety check",
|
||||
)
|
||||
@@ -464,6 +464,28 @@ def run(state, command, args):
|
||||
@option(
|
||||
"--quiet", is_flag=True, help="Quiet standard output, except vulnerability report."
|
||||
)
|
||||
@option("--policy-file", default="", help="Define the policy file to be used")
|
||||
@option(
|
||||
"--exit-code/--continue-on-error",
|
||||
default=True,
|
||||
help="Output standard exit codes. Default: --exit-code",
|
||||
)
|
||||
@option(
|
||||
"--audit-and-monitor/--disable-audit-and-monitor",
|
||||
default=True,
|
||||
help="Send results back to pyup.io for viewing on your dashboard. Requires an API key.",
|
||||
)
|
||||
@option(
|
||||
"--project",
|
||||
default=None,
|
||||
help="Project to associate this scan with on pyup.io. Defaults to a canonicalized github style name if available, otherwise unknown",
|
||||
)
|
||||
@option(
|
||||
"--save-json",
|
||||
default="",
|
||||
help="Path to where output file will be placed, if the path is a directory, "
|
||||
"Safety will use safety-report.json as filename. Default: empty",
|
||||
)
|
||||
@common_options
|
||||
@system_option
|
||||
@pass_state
|
||||
@@ -472,9 +494,14 @@ def check(
|
||||
db=None,
|
||||
style=False,
|
||||
ignore=None,
|
||||
output="default",
|
||||
output="screen",
|
||||
key=None,
|
||||
quiet=False,
|
||||
exit_code=True,
|
||||
policy_file="",
|
||||
save_json="",
|
||||
audit_and_monitor=True,
|
||||
project=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Checks for PyUp Safety security vulnerabilities and against PEP 508 markers provided in Pipfile."""
|
||||
@@ -490,6 +517,11 @@ def check(
|
||||
output=output,
|
||||
key=key,
|
||||
quiet=quiet,
|
||||
exit_code=exit_code,
|
||||
policy_file=policy_file,
|
||||
save_json=save_json,
|
||||
audit_and_monitor=audit_and_monitor,
|
||||
safety_project=project,
|
||||
pypi_mirror=state.pypi_mirror,
|
||||
)
|
||||
|
||||
|
||||
+105
-39
@@ -1,3 +1,4 @@
|
||||
import io
|
||||
import json as simplejson
|
||||
import logging
|
||||
import os
|
||||
@@ -15,7 +16,7 @@ from posixpath import expandvars
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from pipenv import environments, exceptions, pep508checker
|
||||
from pipenv._compat import decode_for_output, fix_utf8
|
||||
from pipenv._compat import fix_utf8
|
||||
from pipenv.patched.pip._internal.build_env import get_runnable_pip
|
||||
from pipenv.patched.pip._internal.exceptions import PipError
|
||||
from pipenv.patched.pip._internal.network.session import PipSession
|
||||
@@ -2842,9 +2843,14 @@ def do_check(
|
||||
system=False,
|
||||
db=None,
|
||||
ignore=None,
|
||||
output="default",
|
||||
output="screen",
|
||||
key=None,
|
||||
quiet=False,
|
||||
exit_code=True,
|
||||
policy_file="",
|
||||
save_json="",
|
||||
audit_and_monitor=True,
|
||||
safety_project=None,
|
||||
pypi_mirror=None,
|
||||
):
|
||||
import json
|
||||
@@ -2860,9 +2866,7 @@ def do_check(
|
||||
pypi_mirror=pypi_mirror,
|
||||
)
|
||||
if not quiet and not project.s.is_quiet():
|
||||
click.echo(
|
||||
click.style(decode_for_output("Checking PEP 508 requirements..."), bold=True)
|
||||
)
|
||||
click.secho("Checking PEP 508 requirements...", bold=True)
|
||||
pep508checker_path = pep508checker.__file__.rstrip("cdo")
|
||||
safety_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "patched", "safety"
|
||||
@@ -2879,7 +2883,7 @@ def do_check(
|
||||
click.echo(
|
||||
"{}\n{}\n{}".format(
|
||||
click.style(
|
||||
decode_for_output("Failed parsing pep508 results: "),
|
||||
"Failed parsing pep508 results: ",
|
||||
fg="white",
|
||||
bold=True,
|
||||
),
|
||||
@@ -2913,12 +2917,11 @@ def do_check(
|
||||
sys.exit(1)
|
||||
else:
|
||||
if not quiet and not project.s.is_quiet():
|
||||
click.echo(click.style("Passed!", fg="green"))
|
||||
click.secho("Passed!", fg="green")
|
||||
if not quiet and not project.s.is_quiet():
|
||||
click.echo(
|
||||
click.style(
|
||||
decode_for_output("Checking installed package safety..."), bold=True
|
||||
)
|
||||
click.secho(
|
||||
"Checking installed packages for vulnerabilities...",
|
||||
bold=True,
|
||||
)
|
||||
if ignore:
|
||||
if not isinstance(ignore, (tuple, list)):
|
||||
@@ -2926,57 +2929,120 @@ def do_check(
|
||||
ignored = [["--ignore", cve] for cve in ignore]
|
||||
if not quiet and not project.s.is_quiet():
|
||||
click.echo(
|
||||
"Notice: Ignoring CVE(s) {}".format(
|
||||
click.style(", ".join(ignore), fg="yellow")
|
||||
"Notice: Ignoring Vulnerabilit{} {}".format(
|
||||
"ies" if len(ignored) > 1 else "y",
|
||||
click.style(", ".join(ignore), fg="yellow"),
|
||||
),
|
||||
err=True,
|
||||
)
|
||||
else:
|
||||
ignored = []
|
||||
|
||||
switch = output
|
||||
if output == "default":
|
||||
switch = "json"
|
||||
options = [
|
||||
"--audit-and-monitor" if audit_and_monitor else "--disable-audit-and-monitor",
|
||||
"--exit-code" if exit_code else "--continue-on-error",
|
||||
]
|
||||
|
||||
if output == "full-report":
|
||||
options.append("--full-report")
|
||||
elif output == "minimal":
|
||||
options.append("--json")
|
||||
elif output not in ["screen", "default"]:
|
||||
options.append(f"--output={output}")
|
||||
|
||||
if save_json:
|
||||
options.append(f"--save-json={save_json}")
|
||||
|
||||
if policy_file:
|
||||
options.append(f"--policy-file={policy_file}")
|
||||
|
||||
if safety_project:
|
||||
options.append(f"--project={safety_project}")
|
||||
|
||||
cmd = _cmd + [safety_path, "check"] + options
|
||||
|
||||
cmd = _cmd + [safety_path, "check", f"--{switch}"]
|
||||
if db:
|
||||
if not quiet and not project.s.is_quiet():
|
||||
click.echo(click.style(f"Using local database {db}"))
|
||||
click.echo(f"Using {db} database")
|
||||
cmd.append(f"--db={db}")
|
||||
elif key or project.s.PIPENV_PYUP_API_KEY:
|
||||
cmd = cmd + [f"--key={key or project.s.PIPENV_PYUP_API_KEY}"]
|
||||
else:
|
||||
PIPENV_SAFETY_DB = (
|
||||
"https://d2qjmgddvqvu75.cloudfront.net/aws/safety/pipenv/1.0.0/"
|
||||
)
|
||||
os.environ["SAFETY_ANNOUNCEMENTS_URL"] = f"{PIPENV_SAFETY_DB}announcements.json"
|
||||
cmd.append(f"--db={PIPENV_SAFETY_DB}")
|
||||
|
||||
if ignored:
|
||||
for cve in ignored:
|
||||
cmd += cve
|
||||
c = run_command(cmd, catch_exceptions=False, is_verbose=project.s.is_verbose())
|
||||
if output == "default":
|
||||
|
||||
os.environ["SAFETY_CUSTOM_INTEGRATION"] = "True"
|
||||
os.environ["SAFETY_SOURCE"] = "pipenv"
|
||||
os.environ["SAFETY_PURE_YAML"] = "True"
|
||||
|
||||
from pipenv.patched.safety.cli import cli
|
||||
|
||||
sys.argv = cmd[1:]
|
||||
|
||||
if output == "minimal":
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
|
||||
code = 0
|
||||
|
||||
with redirect_stdout(io.StringIO()) as out, redirect_stderr(io.StringIO()) as err:
|
||||
try:
|
||||
cli(prog_name="pipenv")
|
||||
except SystemExit as exit_signal:
|
||||
code = exit_signal.code
|
||||
|
||||
report = out.getvalue()
|
||||
error = err.getvalue()
|
||||
|
||||
try:
|
||||
results = simplejson.loads(c.stdout)
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
raise exceptions.JSONParseError(c.stdout, c.stderr)
|
||||
json_report = simplejson.loads(report)
|
||||
except Exception:
|
||||
raise exceptions.PipenvCmdError(
|
||||
cmd_list_to_shell(c.args), c.stdout, c.stderr, c.returncode
|
||||
cmd_list_to_shell(cmd), report, error, exit_code=code
|
||||
)
|
||||
for (package, resolved, installed, description, vuln, *_) in results:
|
||||
meta = json_report.get("report_meta")
|
||||
vulnerabilities_found = meta.get("vulnerabilities_found")
|
||||
|
||||
fg = "green"
|
||||
message = "All good!"
|
||||
db_type = "commercial" if meta.get("api_key", False) else "free"
|
||||
|
||||
if vulnerabilities_found >= 0:
|
||||
fg = "red"
|
||||
message = (
|
||||
f"Scan was complete using Safety’s {db_type} vulnerability database."
|
||||
)
|
||||
|
||||
click.echo()
|
||||
click.secho(f"{vulnerabilities_found} vulnerabilities found.", fg=fg)
|
||||
click.echo()
|
||||
|
||||
vulnerabilities = json_report.get("vulnerabilities", [])
|
||||
|
||||
for vuln in vulnerabilities:
|
||||
click.echo(
|
||||
"{}: {} {} resolved ({} installed)!".format(
|
||||
click.style(vuln, bold=True),
|
||||
click.style(package, fg="green"),
|
||||
click.style(resolved, fg="yellow", bold=False),
|
||||
click.style(installed, fg="yellow", bold=True),
|
||||
"{}: {} {} open to vulnerability {} ({}). More info: {}".format(
|
||||
click.style(vuln["vulnerability_id"], bold=True, fg="red"),
|
||||
click.style(vuln["package_name"], fg="green"),
|
||||
click.style(vuln["analyzed_version"], fg="yellow", bold=True),
|
||||
click.style(vuln["vulnerability_id"], bold=True),
|
||||
click.style(vuln["vulnerable_spec"], fg="yellow", bold=False),
|
||||
click.style(vuln["more_info_url"], bold=True),
|
||||
)
|
||||
)
|
||||
click.echo(f"{description}")
|
||||
click.echo(f"{vuln['advisory']}")
|
||||
click.echo()
|
||||
if c.returncode == 0:
|
||||
click.echo(click.style("All good!", fg="green"))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
else:
|
||||
click.echo(c.stdout)
|
||||
sys.exit(c.returncode)
|
||||
|
||||
click.secho(message, fg="white", bold=True)
|
||||
sys.exit(code)
|
||||
|
||||
cli(prog_name="pipenv")
|
||||
|
||||
|
||||
def do_graph(project, bare=False, json=False, json_tree=False, reverse=False):
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pip==22.3
|
||||
safety==1.10.3
|
||||
safety==2.3.2
|
||||
|
||||
@@ -603,7 +603,7 @@ def is_wheel_installed() -> bool:
|
||||
Return whether the wheel package is installed.
|
||||
"""
|
||||
try:
|
||||
import pipenv.vendor.wheel as wheel # noqa: F401
|
||||
import wheel # noqa: F401
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
2.3.2
|
||||
@@ -2,4 +2,10 @@
|
||||
|
||||
__author__ = """pyup.io"""
|
||||
__email__ = 'support@pyup.io'
|
||||
__version__ = '1.10.3'
|
||||
|
||||
import os
|
||||
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(ROOT, 'VERSION')) as version_file:
|
||||
VERSION = version_file.read().strip()
|
||||
|
||||
@@ -1,48 +1,8 @@
|
||||
"""Allow safety to be executable through `python -m safety`."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
|
||||
PATCHED_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PIPENV_DIR = os.path.dirname(PATCHED_DIR)
|
||||
VENDORED_DIR = os.path.join("PIPENV_DIR", "vendor")
|
||||
|
||||
|
||||
def get_site_packages():
|
||||
prefixes = {sys.prefix, sysconfig.get_config_var('prefix')}
|
||||
try:
|
||||
prefixes.add(sys.real_prefix)
|
||||
except AttributeError:
|
||||
pass
|
||||
form = sysconfig.get_path('purelib', expand=False)
|
||||
py_version_short = '{0[0]}.{0[1]}'.format(sys.version_info)
|
||||
return {
|
||||
form.format(base=prefix, py_version_short=py_version_short)
|
||||
for prefix in prefixes
|
||||
}
|
||||
|
||||
|
||||
def insert_before_site_packages(*paths):
|
||||
site_packages = get_site_packages()
|
||||
index = None
|
||||
for i, path in enumerate(sys.path):
|
||||
if path in site_packages:
|
||||
index = i
|
||||
break
|
||||
if index is None:
|
||||
sys.path += list(paths)
|
||||
else:
|
||||
sys.path = sys.path[:index] + list(paths) + sys.path[index:]
|
||||
|
||||
|
||||
def insert_pipenv_dirs():
|
||||
insert_before_site_packages(os.path.dirname(PIPENV_DIR), PATCHED_DIR, VENDORED_DIR)
|
||||
from pipenv.patched.safety.cli import cli
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
insert_pipenv_dirs()
|
||||
from pipenv.patched.safety.cli import cli
|
||||
cli(prog_name="safety")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import sys
|
||||
import json
|
||||
from typing import Any
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from . import github
|
||||
from pipenv.patched.safety.util import SafetyPolicyFile
|
||||
|
||||
@dataclass
|
||||
class Alert:
|
||||
report: Any
|
||||
key: str
|
||||
policy: Any = None
|
||||
requirements_files: Any = None
|
||||
|
||||
@click.group(help="Send alerts based on the results of a Safety scan.")
|
||||
@click.option('--check-report', help='JSON output of Safety Check to work with.', type=click.File('r'), default=sys.stdin)
|
||||
@click.option("--policy-file", type=SafetyPolicyFile(), default='.safety-policy.yml',
|
||||
help="Define the policy file to be used")
|
||||
@click.option("--key", envvar="SAFETY_API_KEY",
|
||||
help="API Key for pyup.io's vulnerability database. Can be set as SAFETY_API_KEY "
|
||||
"environment variable.", required=True)
|
||||
@click.pass_context
|
||||
def alert(ctx, check_report, policy_file, key):
|
||||
with check_report:
|
||||
# TODO: This breaks --help for subcommands
|
||||
try:
|
||||
safety_report = json.load(check_report)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
click.secho("Error decoding input JSON: {}".format(e.msg), fg='red')
|
||||
sys.exit(1)
|
||||
|
||||
if not 'report_meta' in safety_report:
|
||||
click.secho("You must pass in a valid Safety Check JSON report", fg='red')
|
||||
sys.exit(1)
|
||||
|
||||
ctx.obj = Alert(report=safety_report, policy=policy_file if policy_file else {}, key=key)
|
||||
|
||||
alert.add_command(github.github_pr)
|
||||
alert.add_command(github.github_issue)
|
||||
@@ -0,0 +1,298 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
try:
|
||||
import github as pygithub
|
||||
except ImportError:
|
||||
pygithub = None
|
||||
|
||||
from . import utils, requirements
|
||||
|
||||
def create_branch(repo, base_branch, new_branch):
|
||||
ref = repo.get_git_ref("heads/" + base_branch)
|
||||
repo.create_git_ref(ref="refs/heads/" + new_branch, sha=ref.object.sha)
|
||||
|
||||
def delete_branch(repo, branch):
|
||||
ref = repo.get_git_ref(f"heads/{branch}")
|
||||
ref.delete()
|
||||
|
||||
@click.command()
|
||||
@click.option('--repo', help='GitHub standard repo path (eg, my-org/my-project)')
|
||||
@click.option('--token', help='GitHub Access Token')
|
||||
@click.option('--base-url', help='Optional custom Base URL, if you\'re using GitHub enterprise', default=None)
|
||||
@click.pass_obj
|
||||
@utils.require_files_report
|
||||
def github_pr(obj, repo, token, base_url):
|
||||
"""
|
||||
Create a GitHub PR to fix any vulnerabilities using PyUp's remediation data.
|
||||
|
||||
Normally, this is run by a GitHub action. If you're running this manually, ensure that your local repo is up to date and on HEAD - otherwise you'll see strange results.
|
||||
"""
|
||||
if pygithub is None:
|
||||
click.secho("pygithub is not installed. Did you install Safety with GitHub support? Try pip install safety[github]", fg='red')
|
||||
sys.exit(1)
|
||||
|
||||
# TODO: Improve access to our config in future.
|
||||
branch_prefix = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('branch-prefix', 'pyup/')
|
||||
pr_prefix = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('pr-prefix', '[PyUp] ')
|
||||
assignees = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('assignees', [])
|
||||
labels = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('labels', ['security'])
|
||||
label_severity = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('label-severity', True)
|
||||
ignore_cvss_severity_below = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('ignore-cvss-severity-below', 0)
|
||||
ignore_cvss_unknown_severity = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('ignore-cvss-unknown-severity', False)
|
||||
|
||||
gh = pygithub.Github(token, **({"base_url": base_url} if base_url else {}))
|
||||
repo = gh.get_repo(repo)
|
||||
try:
|
||||
self_user = gh.get_user().login
|
||||
except pygithub.GithubException:
|
||||
# If we're using a token from an action (or integration) we can't call `get_user()`. Fall back
|
||||
# to assuming we're running under an action
|
||||
self_user = "web-flow"
|
||||
|
||||
pulls = repo.get_pulls(state='open', sort='created', base=repo.default_branch)
|
||||
pending_updates = set(obj.report['remediations'].keys())
|
||||
|
||||
# TODO: Refactor this loop into a fn to iterate over remediations nicely
|
||||
for name, contents in obj.requirements_files.items():
|
||||
raw_contents = contents
|
||||
contents = contents.decode('utf-8') # TODO - encoding?
|
||||
parsed_req_file = requirements.RequirementFile(name, contents)
|
||||
for pkg, remediation in obj.report['remediations'].items():
|
||||
if remediation['recommended_version'] is None:
|
||||
print(f"The GitHub PR alerter only currently supports remediations that have a recommended_version: {pkg}")
|
||||
continue
|
||||
|
||||
# We have a single remediation that can have multiple vulnerabilities
|
||||
vulns = [x for x in obj.report['vulnerabilities'] if x['package_name'] == pkg and x['analyzed_version'] == remediation['current_version']]
|
||||
|
||||
if ignore_cvss_unknown_severity and all(x['severity'] is None for x in vulns):
|
||||
print("All vulnerabilities have unknown severity, and ignore_cvss_unknown_severity is set.")
|
||||
continue
|
||||
|
||||
highest_base_score = 0
|
||||
for vuln in vulns:
|
||||
if vuln['severity'] is not None:
|
||||
highest_base_score = max(highest_base_score, (vuln['severity'].get('cvssv3', {}) or {}).get('base_score', 10))
|
||||
|
||||
if ignore_cvss_severity_below:
|
||||
at_least_one_match = False
|
||||
for vuln in vulns:
|
||||
# Consider a None severity as a match, since it's controlled by a different flag
|
||||
# If we can't find a base_score but we have severity data, assume it's critical for now.
|
||||
if vuln['severity'] is None or (vuln['severity'].get('cvssv3', {}) or {}).get('base_score', 10) >= ignore_cvss_severity_below:
|
||||
at_least_one_match = True
|
||||
|
||||
if not at_least_one_match:
|
||||
print(f"None of the vulnerabilities found have a score greater than or equal to the ignore_cvss_severity_below of {ignore_cvss_severity_below}")
|
||||
continue
|
||||
|
||||
for parsed_req in parsed_req_file.requirements:
|
||||
if parsed_req.name == pkg:
|
||||
updated_contents = parsed_req.update_version(contents, remediation['recommended_version'])
|
||||
pending_updates.discard(pkg)
|
||||
|
||||
new_branch = branch_prefix + utils.generate_branch_name(pkg, remediation)
|
||||
skip_create = False
|
||||
|
||||
# Few possible cases:
|
||||
# 1. No existing PRs exist for this change (don't need to handle)
|
||||
# 2. An existing PR exists, and it's out of date (eg, recommended 0.5.1 and we want 0.5.2)
|
||||
# 3. An existing PR exists, and it's not mergable anymore (eg, needs a rebase)
|
||||
# 4. An existing PR exists, and everything's up to date.
|
||||
# 5. An existing PR exists, but it's not needed anymore (perhaps we've been updated to a later version)
|
||||
# 6. No existing PRs exist, but a branch does exist (perhaps the PR was closed but a stale branch left behind)
|
||||
# In any case, we only act if we've been the only committer to the branch.
|
||||
for pr in pulls:
|
||||
if not pr.head.ref.startswith(branch_prefix):
|
||||
continue
|
||||
|
||||
authors = [commit.committer.login for commit in pr.get_commits()]
|
||||
only_us = all([x == self_user for x in authors])
|
||||
|
||||
try:
|
||||
_, pr_pkg, pr_ver = pr.head.ref.split('/')
|
||||
except ValueError:
|
||||
# It's possible that something weird has manually been done, so skip that
|
||||
print('Found an invalid branch name on an open PR, that matches our prefix. Skipping.')
|
||||
continue
|
||||
|
||||
if pr_pkg != pkg:
|
||||
continue
|
||||
|
||||
# Case 4
|
||||
if pr_pkg == pkg and pr_ver == remediation['recommended_version'] and pr.mergeable:
|
||||
print(f"An up to date PR #{pr.number} for {pkg} was found, no action will be taken.")
|
||||
|
||||
skip_create = True
|
||||
continue
|
||||
|
||||
if not only_us:
|
||||
print(f"There are other committers on the PR #{pr.number} for {pkg}. No further action will be taken.")
|
||||
continue
|
||||
|
||||
# Case 2
|
||||
if pr_pkg == pkg and pr_ver != remediation['recommended_version']:
|
||||
print(f"Closing stale PR #{pr.number} for {pkg} as a newer recommended version became")
|
||||
|
||||
pr.create_issue_comment("This PR has been replaced, since a newer recommended version became available.")
|
||||
pr.edit(state='closed')
|
||||
delete_branch(repo, pr.head.ref)
|
||||
|
||||
# Case 3
|
||||
if not pr.mergeable:
|
||||
print(f"Closing PR #{pr.number} for {pkg} as it has become unmergable and we were the only committer")
|
||||
|
||||
pr.create_issue_comment("This PR has been replaced since it became unmergable.")
|
||||
pr.edit(state='closed')
|
||||
delete_branch(repo, pr.head.ref)
|
||||
|
||||
if updated_contents == contents:
|
||||
print(f"Couldn't update {pkg} to {remediation['recommended_version']}")
|
||||
continue
|
||||
|
||||
if skip_create:
|
||||
continue
|
||||
|
||||
try:
|
||||
create_branch(repo, repo.default_branch, new_branch)
|
||||
except pygithub.GithubException as e:
|
||||
if e.data['message'] == "Reference already exists":
|
||||
# There might be a stale branch. If the bot is the only committer, nuke it.
|
||||
comparison = repo.compare(repo.default_branch, new_branch)
|
||||
authors = [commit.committer.login for commit in comparison.commits]
|
||||
only_us = all([x == self_user for x in authors])
|
||||
|
||||
if only_us:
|
||||
delete_branch(repo, new_branch)
|
||||
create_branch(repo, repo.default_branch, new_branch)
|
||||
else:
|
||||
print(f"The branch '{new_branch}' already exists - but there is no matching PR and this branch has committers other than us. This remediation will be skipped.")
|
||||
continue
|
||||
else:
|
||||
raise e
|
||||
|
||||
try:
|
||||
repo.update_file(
|
||||
path=name,
|
||||
message=utils.generate_commit_message(pkg, remediation),
|
||||
content=updated_contents,
|
||||
branch=new_branch,
|
||||
sha=utils.git_sha1(raw_contents)
|
||||
)
|
||||
except pygithub.GithubException as e:
|
||||
if "does not match" in e.data['message']:
|
||||
click.secho(f"GitHub blocked a commit on our branch to the requirements file, {name}, as the local hash we computed didn't match the version on {repo.default_branch}. Make sure you're running safety against the latest code on your default branch.", fg='red')
|
||||
continue
|
||||
else:
|
||||
raise e
|
||||
|
||||
pr = repo.create_pull(title=pr_prefix + utils.generate_title(pkg, remediation, vulns), body=utils.generate_body(pkg, remediation, vulns, api_key=obj.key), head=new_branch, base=repo.default_branch)
|
||||
print(f"Created Pull Request to update {pkg}")
|
||||
|
||||
for assignee in assignees:
|
||||
pr.add_to_assignees(assignee)
|
||||
|
||||
for label in labels:
|
||||
pr.add_to_labels(label)
|
||||
|
||||
if label_severity:
|
||||
score_as_label = utils.cvss3_score_to_label(highest_base_score)
|
||||
if score_as_label:
|
||||
pr.add_to_labels(score_as_label)
|
||||
|
||||
if len(pending_updates) > 0:
|
||||
click.secho("The following remediations were not followed: {}".format(', '.join(pending_updates)), fg='red')
|
||||
|
||||
@click.command()
|
||||
@click.option('--repo', help='GitHub standard repo path (eg, my-org/my-project)')
|
||||
@click.option('--token', help='GitHub Access Token')
|
||||
@click.option('--base-url', help='Optional custom Base URL, if you\'re using GitHub enterprise', default=None)
|
||||
@click.pass_obj
|
||||
@utils.require_files_report # TODO: For now, it can be removed in the future to support env scans.
|
||||
def github_issue(obj, repo, token, base_url):
|
||||
"""
|
||||
Create a GitHub Issue for any vulnerabilities found using PyUp's remediation data.
|
||||
|
||||
Normally, this is run by a GitHub action. If you're running this manually, ensure that your local repo is up to date and on HEAD - otherwise you'll see strange results.
|
||||
"""
|
||||
if pygithub is None:
|
||||
click.secho("pygithub is not installed. Did you install Safety with GitHub support? Try pip install safety[github]", fg='red')
|
||||
sys.exit(1)
|
||||
|
||||
# TODO: Improve access to our config in future.
|
||||
issue_prefix = obj.policy.get('alert', {}).get('security', {}).get('github-issue', {}).get('issue-prefix', '[PyUp] ')
|
||||
assignees = obj.policy.get('alert', {}).get('security', {}).get('github-issue', {}).get('assignees', [])
|
||||
labels = obj.policy.get('alert', {}).get('security', {}).get('github-issue', {}).get('labels', ['security'])
|
||||
label_severity = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('label-severity', True)
|
||||
ignore_cvss_severity_below = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('ignore-cvss-severity-below', 0)
|
||||
ignore_cvss_unknown_severity = obj.policy.get('alert', {}).get('security', {}).get('github-pr', {}).get('ignore-cvss-unknown-severity', False)
|
||||
|
||||
gh = pygithub.Github(token, **({"base_url": base_url} if base_url else {}))
|
||||
repo = gh.get_repo(repo)
|
||||
|
||||
issues = list(repo.get_issues(state='open', sort='created'))
|
||||
ISSUE_TITLE_REGEX = re.escape(issue_prefix) + r"Security Vulnerability in (.+)"
|
||||
|
||||
for name, contents in obj.requirements_files.items():
|
||||
raw_contents = contents
|
||||
contents = contents.decode('utf-8') # TODO - encoding?
|
||||
parsed_req_file = requirements.RequirementFile(name, contents)
|
||||
for pkg, remediation in obj.report['remediations'].items():
|
||||
if remediation['recommended_version'] is None:
|
||||
print(f"The GitHub Issue alerter only currently supports remediations that have a recommended_version: {pkg}")
|
||||
continue
|
||||
|
||||
# We have a single remediation that can have multiple vulnerabilities
|
||||
vulns = [x for x in obj.report['vulnerabilities'] if x['package_name'] == pkg and x['analyzed_version'] == remediation['current_version']]
|
||||
|
||||
if ignore_cvss_unknown_severity and all(x['severity'] is None for x in vulns):
|
||||
print("All vulnerabilities have unknown severity, and ignore_cvss_unknown_severity is set.")
|
||||
continue
|
||||
|
||||
highest_base_score = 0
|
||||
for vuln in vulns:
|
||||
if vuln['severity'] is not None:
|
||||
highest_base_score = max(highest_base_score, (vuln['severity'].get('cvssv3', {}) or {}).get('base_score', 10))
|
||||
|
||||
if ignore_cvss_severity_below:
|
||||
at_least_one_match = False
|
||||
for vuln in vulns:
|
||||
# Consider a None severity as a match, since it's controlled by a different flag
|
||||
# If we can't find a base_score but we have severity data, assume it's critical for now.
|
||||
if vuln['severity'] is None or (vuln['severity'].get('cvssv3', {}) or {}).get('base_score', 10) >= ignore_cvss_severity_below:
|
||||
at_least_one_match = True
|
||||
|
||||
if not at_least_one_match:
|
||||
print(f"None of the vulnerabilities found have a score greater than or equal to the ignore_cvss_severity_below of {ignore_cvss_severity_below}")
|
||||
continue
|
||||
|
||||
for parsed_req in parsed_req_file.requirements:
|
||||
if parsed_req.name == pkg:
|
||||
skip = False
|
||||
for issue in issues:
|
||||
match = re.match(ISSUE_TITLE_REGEX, issue.title)
|
||||
if match:
|
||||
if match.group(1) == pkg:
|
||||
skip = True
|
||||
|
||||
# For now, we just skip issues if they already exist - we don't try and update them.
|
||||
if skip:
|
||||
print(f"An issue already exists for {pkg} - skipping")
|
||||
continue
|
||||
|
||||
pr = repo.create_issue(title=issue_prefix + utils.generate_issue_title(pkg, remediation), body=utils.generate_issue_body(pkg, remediation, vulns, api_key=obj.key))
|
||||
print(f"Created issue to update {pkg}")
|
||||
|
||||
for assignee in assignees:
|
||||
pr.add_to_assignees(assignee)
|
||||
|
||||
for label in labels:
|
||||
pr.add_to_labels(label)
|
||||
|
||||
if label_severity:
|
||||
score_as_label = utils.cvss3_score_to_label(highest_base_score)
|
||||
if score_as_label:
|
||||
pr.add_to_labels(score_as_label)
|
||||
@@ -0,0 +1,339 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,132 @@
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
from functools import wraps
|
||||
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
|
||||
from pathlib import Path
|
||||
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
# Jinja2 will only be installed if the optional deps are installed.
|
||||
# It's fine if our functions fail, but don't let this top level
|
||||
# import error out.
|
||||
try:
|
||||
import jinja2
|
||||
except ImportError:
|
||||
jinja2 = None
|
||||
|
||||
import pipenv.patched.pip._vendor.requests as requests
|
||||
|
||||
|
||||
def highest_base_score(vulns):
|
||||
highest_base_score = 0
|
||||
for vuln in vulns:
|
||||
if vuln['severity'] is not None:
|
||||
highest_base_score = max(highest_base_score, (vuln['severity'].get('cvssv3', {}) or {}).get('base_score', 10))
|
||||
|
||||
return highest_base_score
|
||||
|
||||
def generate_branch_name(pkg, remediation):
|
||||
return pkg + "/" + remediation['recommended_version']
|
||||
|
||||
def generate_issue_title(pkg, remediation):
|
||||
return f"Security Vulnerability in {pkg}"
|
||||
|
||||
def generate_title(pkg, remediation, vulns):
|
||||
suffix = "y" if len(vulns) == 1 else "ies"
|
||||
return f"Update {pkg} from {remediation['current_version']} to {remediation['recommended_version']} to fix {len(vulns)} vulnerabilit{suffix}"
|
||||
|
||||
def generate_body(pkg, remediation, vulns, *, api_key):
|
||||
changelog = fetch_changelog(pkg, remediation['current_version'], remediation['recommended_version'], api_key=api_key)
|
||||
|
||||
p = Path(__file__).parent / 'templates'
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(p)))
|
||||
template = env.get_template('pr.jinja2')
|
||||
|
||||
overall_impact = cvss3_score_to_label(highest_base_score(vulns))
|
||||
result = template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": False })
|
||||
|
||||
# GitHub has a PR body length limit of 65536. If we're going over that, skip the changelog and just use a link.
|
||||
if len(result) > 65500:
|
||||
return template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": True })
|
||||
|
||||
return result
|
||||
|
||||
def generate_issue_body(pkg, remediation, vulns, *, api_key):
|
||||
changelog = fetch_changelog(pkg, remediation['current_version'], remediation['recommended_version'], api_key=api_key)
|
||||
|
||||
p = Path(__file__).parent / 'templates'
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(p)))
|
||||
template = env.get_template('issue.jinja2')
|
||||
|
||||
overall_impact = cvss3_score_to_label(highest_base_score(vulns))
|
||||
result = template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": False })
|
||||
|
||||
# GitHub has a PR body length limit of 65536. If we're going over that, skip the changelog and just use a link.
|
||||
if len(result) > 65500:
|
||||
return template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": True })
|
||||
|
||||
def generate_commit_message(pkg, remediation):
|
||||
return f"Update {pkg} from {remediation['current_version']} to {remediation['recommended_version']}"
|
||||
|
||||
def git_sha1(raw_contents):
|
||||
return hashlib.sha1(b"blob " + str(len(raw_contents)).encode('ascii') + b"\0" + raw_contents).hexdigest()
|
||||
|
||||
def fetch_changelog(package, from_version, to_version, *, api_key):
|
||||
from_version = parse_version(from_version)
|
||||
to_version = parse_version(to_version)
|
||||
changelog = {}
|
||||
|
||||
r = requests.get(
|
||||
"https://pyup.io/api/v1/changelogs/{}/".format(package),
|
||||
headers={"X-Api-Key": api_key}
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
if data:
|
||||
# sort the changelog by release
|
||||
sorted_log = sorted(data.items(), key=lambda v: parse_version(v[0]), reverse=True)
|
||||
|
||||
# go over each release and add it to the log if it's within the "upgrade
|
||||
# range" e.g. update from 1.2 to 1.3 includes a changelog for 1.2.1 but
|
||||
# not for 0.4.
|
||||
for version, log in sorted_log:
|
||||
parsed_version = parse_version(version)
|
||||
if parsed_version > from_version and parsed_version <= to_version:
|
||||
changelog[version] = log
|
||||
|
||||
return changelog
|
||||
|
||||
def cvss3_score_to_label(score):
|
||||
if score >= 0.1 and score <= 3.9:
|
||||
return 'low'
|
||||
elif score >= 4.0 and score <= 6.9:
|
||||
return 'medium'
|
||||
elif score >= 7.0 and score <= 8.9:
|
||||
return 'high'
|
||||
elif score >= 9.0:
|
||||
return 'critical'
|
||||
|
||||
return None
|
||||
|
||||
def require_files_report(func):
|
||||
@wraps(func)
|
||||
def inner(obj, *args, **kwargs):
|
||||
if obj.report['report_meta']['scan_target'] != "files":
|
||||
click.secho("This report was generated against an environment, but this alerter requires a file.", fg='red')
|
||||
sys.exit(1)
|
||||
|
||||
files = obj.report['report_meta']['scanned']
|
||||
obj.requirements_files = {}
|
||||
for f in files:
|
||||
if not os.path.exists(f):
|
||||
cwd = os.getcwd()
|
||||
click.secho("A requirements file scanned in the report, {}, does not exist (looking in {}).".format(f, cwd), fg='red')
|
||||
sys.exit(1)
|
||||
|
||||
obj.requirements_files[f] = open(f, "rb").read()
|
||||
|
||||
return func(obj, *args, **kwargs)
|
||||
return inner
|
||||
+327
-140
@@ -1,23 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
import pipenv.vendor.click as click
|
||||
from pipenv.patched.safety import __version__
|
||||
from pipenv.patched.safety import safety
|
||||
from pipenv.patched.safety.formatter import report, license_report
|
||||
import itertools
|
||||
from pipenv.patched.safety.util import read_requirements, read_vulnerabilities, get_proxy_dict, get_packages_licenses
|
||||
from pipenv.patched.safety.errors import DatabaseFetchError, DatabaseFileNotFoundError, InvalidKeyError, TooManyRequestsError
|
||||
|
||||
try:
|
||||
from json.decoder import JSONDecodeError
|
||||
except ImportError:
|
||||
JSONDecodeError = ValueError
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
from pipenv.patched.safety import safety
|
||||
from pipenv.patched.safety.alerts import alert
|
||||
from pipenv.patched.safety.constants import EXIT_CODE_VULNERABILITIES_FOUND, EXIT_CODE_OK, EXIT_CODE_FAILURE
|
||||
from pipenv.patched.safety.errors import SafetyException, SafetyError
|
||||
from pipenv.patched.safety.formatter import SafetyFormatter
|
||||
from pipenv.patched.safety.output_utils import should_add_nl
|
||||
from pipenv.patched.safety.safety import get_packages, read_vulnerabilities, fetch_policy, post_results
|
||||
from pipenv.patched.safety.util import get_proxy_dict, get_packages_licenses, output_exception, \
|
||||
MutuallyExclusiveOption, DependentOption, transform_ignore, SafetyPolicyFile, active_color_if_needed, \
|
||||
get_processed_options, get_safety_version, json_alias, bare_alias, SafetyContext, is_a_remote_mirror, \
|
||||
filter_announcements
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version=__version__)
|
||||
def cli():
|
||||
pass
|
||||
@click.option('--debug/--no-debug', default=False)
|
||||
@click.option('--telemetry/--disable-telemetry', default=True, hidden=True)
|
||||
@click.option('--disable-optional-telemetry-data', default=False, cls=MutuallyExclusiveOption,
|
||||
mutually_exclusive=["telemetry", "disable-telemetry"], is_flag=True, show_default=True)
|
||||
@click.version_option(version=get_safety_version())
|
||||
@click.pass_context
|
||||
def cli(ctx, debug, telemetry, disable_optional_telemetry_data):
|
||||
"""
|
||||
Safety checks Python dependencies for known security vulnerabilities and suggests the proper
|
||||
remediations for vulnerabilities detected. Safety can be run on developer machines, in CI/CD pipelines and
|
||||
on production systems.
|
||||
"""
|
||||
SafetyContext().safety_source = 'cli'
|
||||
ctx.telemetry = telemetry and not disable_optional_telemetry_data
|
||||
level = logging.CRITICAL
|
||||
if debug:
|
||||
level = logging.DEBUG
|
||||
|
||||
logging.basicConfig(format='%(asctime)s %(name)s => %(message)s', level=level)
|
||||
|
||||
LOG.info(f'Telemetry enabled: {ctx.telemetry}')
|
||||
|
||||
@ctx.call_on_close
|
||||
def clean_up_on_close():
|
||||
LOG.debug('Calling clean up on close function.')
|
||||
safety.close_session()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@@ -25,101 +58,197 @@ def cli():
|
||||
help="API Key for pyup.io's vulnerability database. Can be set as SAFETY_API_KEY "
|
||||
"environment variable. Default: empty")
|
||||
@click.option("--db", default="",
|
||||
help="Path to a local vulnerability database. Default: empty")
|
||||
@click.option("--json/--no-json", default=False,
|
||||
help="Output vulnerabilities in JSON format. Default: --no-json")
|
||||
@click.option("--full-report/--short-report", default=False,
|
||||
help='Full reports include a security advisory (if available). Default: '
|
||||
'--short-report')
|
||||
@click.option("--bare/--not-bare", default=False,
|
||||
help='Output vulnerable packages only. '
|
||||
'Useful in combination with other tools. '
|
||||
'Default: --not-bare')
|
||||
@click.option("--cache/--no-cache", default=False,
|
||||
help="Cache requests to the vulnerability database locally. Default: --no-cache")
|
||||
@click.option("--stdin/--no-stdin", default=False,
|
||||
help="Read input from stdin. Default: --no-stdin")
|
||||
@click.option("files", "--file", "-r", multiple=True, type=click.File(),
|
||||
help="Path to a local or remote vulnerability database. Default: empty")
|
||||
@click.option("--full-report/--short-report", default=False, cls=MutuallyExclusiveOption,
|
||||
mutually_exclusive=["output", "json", "bare"],
|
||||
with_values={"output": ['json', 'bare'], "json": [True, False], "bare": [True, False]},
|
||||
help='Full reports include a security advisory (if available). Default: --short-report')
|
||||
@click.option("--cache", is_flag=False, flag_value=60, default=0,
|
||||
help="Cache requests to the vulnerability database locally. Default: 0 seconds",
|
||||
hidden=True)
|
||||
@click.option("--stdin", default=False, cls=MutuallyExclusiveOption, mutually_exclusive=["files"],
|
||||
help="Read input from stdin.", is_flag=True, show_default=True)
|
||||
@click.option("files", "--file", "-r", multiple=True, type=click.File(), cls=MutuallyExclusiveOption,
|
||||
mutually_exclusive=["stdin"],
|
||||
help="Read input from one (or multiple) requirement files. Default: empty")
|
||||
@click.option("ignore", "--ignore", "-i", multiple=True, type=str, default=[],
|
||||
@click.option("--ignore", "-i", multiple=True, type=str, default=[], callback=transform_ignore,
|
||||
help="Ignore one (or multiple) vulnerabilities by ID. Default: empty")
|
||||
@click.option("--output", "-o", default="",
|
||||
help="Path to where output file will be placed. Default: empty")
|
||||
@click.option("proxyhost", "--proxy-host", "-ph", multiple=False, type=str, default=None,
|
||||
help="Proxy host IP or DNS --proxy-host")
|
||||
@click.option("proxyport", "--proxy-port", "-pp", multiple=False, type=int, default=80,
|
||||
help="Proxy port number --proxy-port")
|
||||
@click.option("proxyprotocol", "--proxy-protocol", "-pr", multiple=False, type=str, default='http',
|
||||
@click.option('--json', default=False, cls=MutuallyExclusiveOption, mutually_exclusive=["output", "bare"],
|
||||
with_values={"output": ['screen', 'text', 'bare', 'json'], "bare": [True, False]}, callback=json_alias,
|
||||
hidden=True, is_flag=True, show_default=True)
|
||||
@click.option('--bare', default=False, cls=MutuallyExclusiveOption, mutually_exclusive=["output", "json"],
|
||||
with_values={"output": ['screen', 'text', 'bare', 'json'], "json": [True, False]}, callback=bare_alias,
|
||||
hidden=True, is_flag=True, show_default=True)
|
||||
@click.option('--output', "-o", type=click.Choice(['screen', 'text', 'json', 'bare'], case_sensitive=False),
|
||||
default='screen', callback=active_color_if_needed, envvar='SAFETY_OUTPUT')
|
||||
@click.option("--proxy-protocol", "-pr", type=click.Choice(['http', 'https']), default='https', cls=DependentOption, required_options=['proxy_host'],
|
||||
help="Proxy protocol (https or http) --proxy-protocol")
|
||||
def check(key, db, json, full_report, bare, stdin, files, cache, ignore, output, proxyprotocol, proxyhost, proxyport):
|
||||
if files and stdin:
|
||||
click.secho("Can't read from --stdin and --file at the same time, exiting", fg="red", file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
@click.option("--proxy-host", "-ph", multiple=False, type=str, default=None,
|
||||
help="Proxy host IP or DNS --proxy-host")
|
||||
@click.option("--proxy-port", "-pp", multiple=False, type=int, default=80, cls=DependentOption, required_options=['proxy_host'],
|
||||
help="Proxy port number --proxy-port")
|
||||
@click.option("--exit-code/--continue-on-error", default=True,
|
||||
help="Output standard exit codes. Default: --exit-code")
|
||||
@click.option("--policy-file", type=SafetyPolicyFile(), default='.safety-policy.yml',
|
||||
help="Define the policy file to be used")
|
||||
@click.option("--audit-and-monitor/--disable-audit-and-monitor", default=True,
|
||||
help="Send results back to pyup.io for viewing on your dashboard. Requires an API key.")
|
||||
@click.option("--project", default=None,
|
||||
help="Project to associate this scan with on pyup.io. Defaults to a canonicalized github style name if available, otherwise unknown")
|
||||
|
||||
@click.option("--save-json", default="", help="Path to where output file will be placed, if the path is a directory, "
|
||||
"Safety will use safety-report.json as filename. Default: empty")
|
||||
@click.pass_context
|
||||
def check(ctx, key, db, full_report, stdin, files, cache, ignore, output, json, bare, proxy_protocol, proxy_host, proxy_port,
|
||||
exit_code, policy_file, save_json, audit_and_monitor, project):
|
||||
"""
|
||||
Find vulnerabilities in Python dependencies at the target provided.
|
||||
|
||||
"""
|
||||
LOG.info('Running check command')
|
||||
|
||||
if files:
|
||||
packages = list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
|
||||
elif stdin:
|
||||
packages = list(read_requirements(sys.stdin))
|
||||
else:
|
||||
import pipenv.patched.pip._vendor.pkg_resources as pkg_resources
|
||||
packages = [
|
||||
d for d in pkg_resources.working_set
|
||||
if d.key not in {"python", "wsgiref", "argparse"}
|
||||
]
|
||||
proxy_dictionary = get_proxy_dict(proxyprotocol, proxyhost, proxyport)
|
||||
try:
|
||||
vulns = safety.check(packages=packages, key=key, db_mirror=db, cached=cache, ignore_ids=ignore, proxy=proxy_dictionary)
|
||||
output_report = report(vulns=vulns,
|
||||
full=full_report,
|
||||
json_report=json,
|
||||
bare_report=bare,
|
||||
checked_packages=len(packages),
|
||||
db=db,
|
||||
key=key)
|
||||
packages = get_packages(files, stdin)
|
||||
proxy_dictionary = get_proxy_dict(proxy_protocol, proxy_host, proxy_port)
|
||||
|
||||
if output:
|
||||
with open(output, 'w+') as output_file:
|
||||
output_file.write(output_report)
|
||||
if key:
|
||||
server_policies = fetch_policy(key=key, proxy=proxy_dictionary)
|
||||
server_audit_and_monitor = server_policies["audit_and_monitor"]
|
||||
server_safety_policy = server_policies["safety_policy"]
|
||||
else:
|
||||
click.secho(output_report, nl=False if bare and not vulns else True)
|
||||
sys.exit(-1 if vulns else 0)
|
||||
except InvalidKeyError:
|
||||
click.secho("Your API Key '{key}' is invalid. See {link}".format(
|
||||
key=key, link='https://goo.gl/O7Y1rS'),
|
||||
fg="red",
|
||||
file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
except DatabaseFileNotFoundError:
|
||||
click.secho("Unable to load vulnerability database from {db}".format(db=db), fg="red", file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
except DatabaseFetchError:
|
||||
click.secho("Unable to load vulnerability database", fg="red", file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
server_audit_and_monitor = False
|
||||
server_safety_policy = ""
|
||||
|
||||
if server_safety_policy and policy_file:
|
||||
click.secho(
|
||||
"Warning: both a local policy file '{policy_filename}' and a server sent policy are present. "
|
||||
"Continuing with the local policy file.".format(policy_filename=policy_file['filename']),
|
||||
fg="yellow",
|
||||
file=sys.stderr
|
||||
)
|
||||
elif server_safety_policy:
|
||||
with tempfile.NamedTemporaryFile(prefix='server-safety-policy-') as tmp:
|
||||
tmp.write(server_safety_policy.encode('utf-8'))
|
||||
tmp.seek(0)
|
||||
|
||||
policy_file = SafetyPolicyFile().convert(tmp.name, param=None, ctx=None)
|
||||
LOG.info('Using server side policy file')
|
||||
|
||||
ignore_severity_rules = None
|
||||
ignore, ignore_severity_rules, exit_code = get_processed_options(policy_file, ignore,
|
||||
ignore_severity_rules, exit_code)
|
||||
|
||||
is_env_scan = not stdin and not files
|
||||
params = {'stdin': stdin, 'files': files, 'policy_file': policy_file, 'continue_on_error': not exit_code,
|
||||
'ignore_severity_rules': ignore_severity_rules, 'project': project, 'audit_and_monitor': server_audit_and_monitor and audit_and_monitor}
|
||||
LOG.info('Calling the check function')
|
||||
vulns, db_full = safety.check(packages=packages, key=key, db_mirror=db, cached=cache, ignore_vulns=ignore,
|
||||
ignore_severity_rules=ignore_severity_rules, proxy=proxy_dictionary,
|
||||
include_ignored=True, is_env_scan=is_env_scan, telemetry=ctx.parent.telemetry,
|
||||
params=params)
|
||||
LOG.debug('Vulnerabilities returned: %s', vulns)
|
||||
LOG.debug('full database returned is None: %s', db_full is None)
|
||||
|
||||
LOG.info('Safety is going to calculate remediations')
|
||||
remediations = safety.calculate_remediations(vulns, db_full)
|
||||
|
||||
announcements = []
|
||||
if not db or is_a_remote_mirror(db):
|
||||
LOG.info('Not local DB used, Getting announcements')
|
||||
announcements = safety.get_announcements(key=key, proxy=proxy_dictionary, telemetry=ctx.parent.telemetry)
|
||||
|
||||
json_report = None
|
||||
if save_json or (server_audit_and_monitor and audit_and_monitor):
|
||||
default_name = 'safety-report.json'
|
||||
json_report = SafetyFormatter(output='json').render_vulnerabilities(announcements, vulns, remediations,
|
||||
full_report, packages)
|
||||
|
||||
if server_audit_and_monitor and audit_and_monitor:
|
||||
policy_contents = ''
|
||||
if policy_file:
|
||||
policy_contents = policy_file.get('raw', '')
|
||||
|
||||
r = post_results(key=key, proxy=proxy_dictionary, safety_json=json_report, policy_file=policy_contents)
|
||||
SafetyContext().params['audit_and_monitor_url'] = r.get('url')
|
||||
|
||||
if save_json:
|
||||
if os.path.isdir(save_json):
|
||||
save_json = os.path.join(save_json, default_name)
|
||||
|
||||
with open(save_json, 'w+') as output_json_file:
|
||||
output_json_file.write(json_report)
|
||||
|
||||
LOG.info('Safety is going to render the vulnerabilities report using %s output', output)
|
||||
if json_report and output == 'json':
|
||||
output_report = json_report
|
||||
else:
|
||||
output_report = SafetyFormatter(output=output).render_vulnerabilities(announcements, vulns, remediations,
|
||||
full_report, packages)
|
||||
|
||||
# Announcements are send to stderr if not terminal, it doesn't depend on "exit_code" value
|
||||
stderr_announcements = filter_announcements(announcements=announcements, by_type='error')
|
||||
if stderr_announcements and (not sys.stdout.isatty() and os.environ.get("SAFETY_OS_DESCRIPTION", None) != 'run'):
|
||||
LOG.info('sys.stdout is not a tty, error announcements are going to be send to stderr')
|
||||
click.secho(SafetyFormatter(output='text').render_announcements(stderr_announcements), fg="red",
|
||||
file=sys.stderr)
|
||||
|
||||
found_vulns = list(filter(lambda v: not v.ignored, vulns))
|
||||
LOG.info('Vulnerabilities found (Not ignored): %s', len(found_vulns))
|
||||
LOG.info('All vulnerabilities found (ignored and Not ignored): %s', len(vulns))
|
||||
|
||||
click.secho(output_report, nl=should_add_nl(output, found_vulns), file=sys.stdout)
|
||||
|
||||
if exit_code and found_vulns:
|
||||
LOG.info('Exiting with default code for vulnerabilities found')
|
||||
sys.exit(EXIT_CODE_VULNERABILITIES_FOUND)
|
||||
|
||||
sys.exit(EXIT_CODE_OK)
|
||||
|
||||
except SafetyError as e:
|
||||
LOG.exception('Expected SafetyError happened: %s', e)
|
||||
output_exception(e, exit_code_output=exit_code)
|
||||
except Exception as e:
|
||||
LOG.exception('Unexpected Exception happened: %s', e)
|
||||
exception = e if isinstance(e, SafetyException) else SafetyException(info=e)
|
||||
output_exception(exception, exit_code_output=exit_code)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--full-report/--short-report", default=False,
|
||||
@click.option("--full-report/--short-report", default=False, cls=MutuallyExclusiveOption, mutually_exclusive=["output"], with_values={"output": ['json', 'bare']},
|
||||
help='Full reports include a security advisory (if available). Default: '
|
||||
'--short-report')
|
||||
@click.option("--bare/--not-bare", default=False,
|
||||
help='Output vulnerable packages only. Useful in combination with other tools. '
|
||||
'Default: --not-bare')
|
||||
@click.option('--output', "-o", type=click.Choice(['screen', 'text', 'json', 'bare'], case_sensitive=False),
|
||||
default='screen', callback=active_color_if_needed)
|
||||
@click.option("file", "--file", "-f", type=click.File(), required=True,
|
||||
help="Read input from an insecure report file. Default: empty")
|
||||
def review(full_report, bare, file):
|
||||
if full_report and bare:
|
||||
click.secho("Can't choose both --bare and --full-report/--short-report", fg="red")
|
||||
sys.exit(-1)
|
||||
@click.pass_context
|
||||
def review(ctx, full_report, output, file):
|
||||
"""
|
||||
Show an output from a previous exported JSON report.
|
||||
"""
|
||||
LOG.info('Running check command')
|
||||
report = {}
|
||||
|
||||
try:
|
||||
input_vulns = read_vulnerabilities(file)
|
||||
except JSONDecodeError:
|
||||
click.secho("Not a valid JSON file", fg="red")
|
||||
sys.exit(-1)
|
||||
report = read_vulnerabilities(file)
|
||||
except SafetyError as e:
|
||||
LOG.exception('Expected SafetyError happened: %s', e)
|
||||
output_exception(e, exit_code_output=True)
|
||||
except Exception as e:
|
||||
LOG.exception('Unexpected Exception happened: %s', e)
|
||||
exception = e if isinstance(e, SafetyException) else SafetyException(info=e)
|
||||
output_exception(exception, exit_code_output=True)
|
||||
|
||||
vulns = safety.review(input_vulns)
|
||||
output_report = report(vulns=vulns, full=full_report, bare_report=bare)
|
||||
click.secho(output_report, nl=False if bare and not vulns else True)
|
||||
params = {'file': file}
|
||||
vulns, remediations, packages = safety.review(report, params=params)
|
||||
|
||||
announcements = safety.get_announcements(key=None, proxy=None, telemetry=ctx.parent.telemetry)
|
||||
output_report = SafetyFormatter(output=output).render_vulnerabilities(announcements, vulns, remediations,
|
||||
full_report, packages)
|
||||
|
||||
found_vulns = list(filter(lambda v: not v.ignored, vulns))
|
||||
click.secho(output_report, nl=should_add_nl(output, found_vulns), file=sys.stdout)
|
||||
sys.exit(EXIT_CODE_OK)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@@ -128,15 +257,11 @@ def review(full_report, bare, file):
|
||||
"environment variable. Default: empty")
|
||||
@click.option("--db", default="",
|
||||
help="Path to a local license database. Default: empty")
|
||||
@click.option("--json/--no-json", default=False,
|
||||
help="Output packages licenses in JSON format. Default: --no-json")
|
||||
@click.option("--bare/--not-bare", default=False,
|
||||
help='Output packages licenses names only. '
|
||||
'Useful in combination with other tools. '
|
||||
'Default: --not-bare')
|
||||
@click.option("--cache/--no-cache", default=True,
|
||||
@click.option('--output', "-o", type=click.Choice(['screen', 'text', 'json', 'bare'], case_sensitive=False),
|
||||
default='screen')
|
||||
@click.option("--cache", default=0,
|
||||
help='Whether license database file should be cached.'
|
||||
'Default: --cache')
|
||||
'Default: 0 seconds')
|
||||
@click.option("files", "--file", "-r", multiple=True, type=click.File(),
|
||||
help="Read input from one (or multiple) requirement files. Default: empty")
|
||||
@click.option("proxyhost", "--proxy-host", "-ph", multiple=False, type=str, default=None,
|
||||
@@ -145,50 +270,112 @@ def review(full_report, bare, file):
|
||||
help="Proxy port number --proxy-port")
|
||||
@click.option("proxyprotocol", "--proxy-protocol", "-pr", multiple=False, type=str, default='http',
|
||||
help="Proxy protocol (https or http) --proxy-protocol")
|
||||
def license(key, db, json, bare, cache, files, proxyprotocol, proxyhost, proxyport):
|
||||
@click.pass_context
|
||||
def license(ctx, key, db, output, cache, files, proxyprotocol, proxyhost, proxyport):
|
||||
"""
|
||||
Find the open source licenses used by your Python dependencies.
|
||||
"""
|
||||
LOG.info('Running license command')
|
||||
packages = get_packages(files, False)
|
||||
|
||||
if files:
|
||||
packages = list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
|
||||
else:
|
||||
import pipenv.patched.pip._vendor.pkg_resources as pkg_resources
|
||||
packages = [
|
||||
d for d in pkg_resources.working_set
|
||||
if d.key not in {"python", "wsgiref", "argparse"}
|
||||
]
|
||||
|
||||
proxy_dictionary = get_proxy_dict(proxyprotocol, proxyhost, proxyport)
|
||||
licenses_db = {}
|
||||
|
||||
try:
|
||||
licenses_db = safety.get_licenses(key, db, cache, proxy_dictionary)
|
||||
except InvalidKeyError as invalid_key_error:
|
||||
if str(invalid_key_error):
|
||||
message = str(invalid_key_error)
|
||||
else:
|
||||
message = "Your API Key '{key}' is invalid. See {link}".format(
|
||||
key=key, link='https://goo.gl/O7Y1rS'
|
||||
)
|
||||
click.secho(message, fg="red", file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
except DatabaseFileNotFoundError:
|
||||
click.secho("Unable to load licenses database from {db}".format(db=db), fg="red", file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
except TooManyRequestsError:
|
||||
click.secho("Unable to load licenses database (Too many requests, please wait before another request)",
|
||||
fg="red",
|
||||
file=sys.stderr
|
||||
)
|
||||
sys.exit(-1)
|
||||
except DatabaseFetchError:
|
||||
click.secho("Unable to load licenses database", fg="red", file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
filtered_packages_licenses = get_packages_licenses(packages, licenses_db)
|
||||
output_report = license_report(
|
||||
packages=packages,
|
||||
licenses=filtered_packages_licenses,
|
||||
json_report=json,
|
||||
bare_report=bare
|
||||
)
|
||||
licenses_db = safety.get_licenses(key=key, db_mirror=db, cached=cache, proxy=proxy_dictionary,
|
||||
telemetry=ctx.parent.telemetry)
|
||||
except SafetyError as e:
|
||||
LOG.exception('Expected SafetyError happened: %s', e)
|
||||
output_exception(e, exit_code_output=False)
|
||||
except Exception as e:
|
||||
LOG.exception('Unexpected Exception happened: %s', e)
|
||||
exception = e if isinstance(e, SafetyException) else SafetyException(info=e)
|
||||
output_exception(exception, exit_code_output=False)
|
||||
|
||||
filtered_packages_licenses = get_packages_licenses(packages=packages, licenses_db=licenses_db)
|
||||
|
||||
announcements = []
|
||||
if not db:
|
||||
announcements = safety.get_announcements(key=key, proxy=proxy_dictionary, telemetry=ctx.parent.telemetry)
|
||||
|
||||
output_report = SafetyFormatter(output=output).render_licenses(announcements, filtered_packages_licenses)
|
||||
|
||||
click.secho(output_report, nl=True)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--path", default=".", help="Path where the generated file will be saved. Default: current directory")
|
||||
@click.argument('name')
|
||||
@click.pass_context
|
||||
def generate(ctx, name, path):
|
||||
"""Create a boilerplate supported file type.
|
||||
|
||||
NAME is the name of the file type to generate. Valid values are: policy_file
|
||||
"""
|
||||
if name != 'policy_file':
|
||||
click.secho(f'This Safety version only supports "policy_file" generation. "{name}" is not supported.', fg='red',
|
||||
file=sys.stderr)
|
||||
sys.exit(EXIT_CODE_FAILURE)
|
||||
|
||||
LOG.info('Running generate %s', name)
|
||||
|
||||
if not os.path.exists(path):
|
||||
click.secho(f'The path "{path}" does not exist.', fg='red',
|
||||
file=sys.stderr)
|
||||
sys.exit(EXIT_CODE_FAILURE)
|
||||
|
||||
policy = os.path.join(path, '.safety-policy.yml')
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
try:
|
||||
with open(policy, "w") as f:
|
||||
f.write(open(os.path.join(ROOT, 'safety-policy-template.yml')).read())
|
||||
LOG.debug('Safety created the policy file.')
|
||||
msg = f'A default Safety policy file has been generated! Review the file contents in the path {path} in the ' \
|
||||
'file: .safety-policy.yml'
|
||||
click.secho(msg, fg='green')
|
||||
except Exception as exc:
|
||||
if isinstance(exc, OSError):
|
||||
LOG.debug('Unable to generate %s because: %s', name, exc.errno)
|
||||
|
||||
click.secho(f'Unable to generate {name}, because: {str(exc)} error.', fg='red',
|
||||
file=sys.stderr)
|
||||
sys.exit(EXIT_CODE_FAILURE)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--path", default=".safety-policy.yml", help="Path where the generated file will be saved. Default: current directory")
|
||||
@click.argument('name')
|
||||
@click.pass_context
|
||||
def validate(ctx, name, path):
|
||||
"""Verify the validity of a supported file type.
|
||||
|
||||
NAME is the name of the file type to validate. Valid values are: policy_file
|
||||
"""
|
||||
if name != 'policy_file':
|
||||
click.secho(f'This Safety version only supports "policy_file" validation. "{name}" is not supported.', fg='red',
|
||||
file=sys.stderr)
|
||||
sys.exit(EXIT_CODE_FAILURE)
|
||||
|
||||
LOG.info('Running validate %s', name)
|
||||
|
||||
if not os.path.exists(path):
|
||||
click.secho(f'The path "{path}" does not exist.', fg='red', file=sys.stderr)
|
||||
sys.exit(EXIT_CODE_FAILURE)
|
||||
|
||||
try:
|
||||
values = SafetyPolicyFile().convert(path, None, None)
|
||||
except Exception as e:
|
||||
click.secho(str(e).lstrip(), fg='red', file=sys.stderr)
|
||||
sys.exit(EXIT_CODE_FAILURE)
|
||||
|
||||
del values['raw']
|
||||
|
||||
click.secho(f'The Safety policy file was successfully parsed with the following values:', fg='green')
|
||||
click.secho(json.dumps(values, indent=4, default=str))
|
||||
|
||||
cli.add_command(alert)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
||||
@@ -2,21 +2,37 @@
|
||||
import os
|
||||
|
||||
OPEN_MIRRORS = [
|
||||
"https://raw.githubusercontent.com/pyupio/safety-db/master/data/",
|
||||
"https://pyup.io/aws/safety/free/",
|
||||
]
|
||||
|
||||
API_VERSION = 'v1/'
|
||||
SAFETY_ENDPOINT = 'safety/'
|
||||
API_BASE_URL = 'https://pyup.io/api/' + API_VERSION + SAFETY_ENDPOINT
|
||||
|
||||
API_MIRRORS = [
|
||||
"https://pyup.io/api/v1/safety/"
|
||||
API_BASE_URL
|
||||
]
|
||||
|
||||
REQUEST_TIMEOUT = 5
|
||||
|
||||
CACHE_VALID_SECONDS = 60 * 60 * 2 # 2 hours
|
||||
|
||||
CACHE_LICENSES_VALID_SECONDS = 60 * 60 * 24 * 7 # one week
|
||||
|
||||
CACHE_FILE = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
".safety",
|
||||
"cache.json"
|
||||
)
|
||||
|
||||
# Colors
|
||||
YELLOW = 'yellow'
|
||||
RED = 'red'
|
||||
GREEN = 'green'
|
||||
|
||||
|
||||
# Exit codes
|
||||
EXIT_CODE_OK = 0
|
||||
EXIT_CODE_FAILURE = 1
|
||||
EXIT_CODE_VULNERABILITIES_FOUND = 64
|
||||
EXIT_CODE_INVALID_API_KEY = 65
|
||||
EXIT_CODE_TOO_MANY_REQUESTS = 66
|
||||
EXIT_CODE_UNABLE_TO_LOAD_LOCAL_VULNERABILITY_DB = 67
|
||||
EXIT_CODE_UNABLE_TO_FETCH_VULNERABILITY_DB = 68
|
||||
EXIT_CODE_MALFORMED_DB = 69
|
||||
|
||||
@@ -1,14 +1,106 @@
|
||||
class DatabaseFetchError(Exception):
|
||||
pass
|
||||
from pipenv.patched.safety.constants import EXIT_CODE_FAILURE, EXIT_CODE_INVALID_API_KEY, EXIT_CODE_TOO_MANY_REQUESTS, \
|
||||
EXIT_CODE_UNABLE_TO_FETCH_VULNERABILITY_DB, EXIT_CODE_UNABLE_TO_LOAD_LOCAL_VULNERABILITY_DB, EXIT_CODE_MALFORMED_DB
|
||||
|
||||
|
||||
class SafetyException(Exception):
|
||||
|
||||
def __init__(self, message="Unhandled exception happened: {info}", info=""):
|
||||
self.message = message.format(info=info)
|
||||
super().__init__(self.message)
|
||||
|
||||
def get_exit_code(self):
|
||||
return EXIT_CODE_FAILURE
|
||||
|
||||
|
||||
class SafetyError(Exception):
|
||||
|
||||
def __init__(self, message="Unhandled Safety generic error"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
def get_exit_code(self):
|
||||
return EXIT_CODE_FAILURE
|
||||
|
||||
|
||||
class MalformedDatabase(SafetyError):
|
||||
|
||||
def __init__(self, reason=None, fetched_from="server",
|
||||
message="Sorry, something went wrong.\n" +
|
||||
"Safety CLI can not read the data fetched from {fetched_from} because is malformed.\n"):
|
||||
info = "Reason, {reason}".format(reason=reason)
|
||||
self.message = message.format(fetched_from=fetched_from) + (info if reason else "")
|
||||
super().__init__(self.message)
|
||||
|
||||
def get_exit_code(self):
|
||||
return EXIT_CODE_MALFORMED_DB
|
||||
|
||||
|
||||
class DatabaseFetchError(SafetyError):
|
||||
|
||||
def __init__(self, message="Unable to load vulnerability database"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
def get_exit_code(self):
|
||||
return EXIT_CODE_UNABLE_TO_FETCH_VULNERABILITY_DB
|
||||
|
||||
|
||||
class DatabaseFileNotFoundError(DatabaseFetchError):
|
||||
pass
|
||||
|
||||
def __init__(self, db=None, message="Unable to find vulnerability database in {db}"):
|
||||
self.db = db
|
||||
self.message = message.format(db=db)
|
||||
super().__init__(self.message)
|
||||
|
||||
def get_exit_code(self):
|
||||
return EXIT_CODE_UNABLE_TO_LOAD_LOCAL_VULNERABILITY_DB
|
||||
|
||||
|
||||
class InvalidKeyError(DatabaseFetchError):
|
||||
pass
|
||||
|
||||
def __init__(self, key=None, message="Your API Key '{key}' is invalid. See {link}.", reason=None):
|
||||
self.key = key
|
||||
self.link = 'https://bit.ly/3OY2wEI'
|
||||
self.message = message.format(key=key, link=self.link) if key else message
|
||||
info = f" Reason: {reason}"
|
||||
self.message = self.message + (info if reason else "")
|
||||
super().__init__(self.message)
|
||||
|
||||
def get_exit_code(self):
|
||||
return EXIT_CODE_INVALID_API_KEY
|
||||
|
||||
|
||||
class TooManyRequestsError(DatabaseFetchError):
|
||||
pass
|
||||
|
||||
def __init__(self, reason=None,
|
||||
message="Too many requests."):
|
||||
info = f" Reason: {reason}"
|
||||
self.message = message + (info if reason else "")
|
||||
super().__init__(self.message)
|
||||
|
||||
def get_exit_code(self):
|
||||
return EXIT_CODE_TOO_MANY_REQUESTS
|
||||
|
||||
|
||||
class NetworkConnectionError(DatabaseFetchError):
|
||||
|
||||
def __init__(self, message="Check your network connection, unable to reach the server."):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class RequestTimeoutError(DatabaseFetchError):
|
||||
|
||||
def __init__(self, message="Check your network connection, the request timed out."):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class ServerError(DatabaseFetchError):
|
||||
|
||||
def __init__(self, reason=None,
|
||||
message="Sorry, something went wrong.\n" + "Safety CLI can not connect to the server.\n" +
|
||||
"Our engineers are working quickly to resolve the issue."):
|
||||
info = f" Reason: {reason}"
|
||||
self.message = message + (info if reason else "")
|
||||
super().__init__(self.message)
|
||||
|
||||
@@ -1,342 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import platform
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
import logging
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from .util import get_packages_licenses
|
||||
NOT_IMPLEMENTED = "You should implement this."
|
||||
|
||||
# python 2.7 compat
|
||||
try:
|
||||
FileNotFoundError
|
||||
except NameError:
|
||||
FileNotFoundError = IOError
|
||||
|
||||
try:
|
||||
system = platform.system()
|
||||
python_version = ".".join([str(i) for i in sys.version_info[0:2]])
|
||||
# get_terminal_size exists on Python 3.4 but isn't working on windows
|
||||
if system == "Windows" and python_version in ["3.4"]:
|
||||
raise ImportError
|
||||
from shutil import get_terminal_size
|
||||
except ImportError:
|
||||
# fallback for python < 3
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
|
||||
def get_terminal_size():
|
||||
size = namedtuple("_", ["rows", "columns"])
|
||||
try:
|
||||
rows, columns = subprocess.check_output(
|
||||
['stty', 'size'],
|
||||
stderr=subprocess.STDOUT
|
||||
).split()
|
||||
return size(rows=int(rows), columns=int(columns))
|
||||
# this won't work
|
||||
# - on windows (FileNotFoundError/OSError)
|
||||
# - python 2.6 (AttributeError)
|
||||
# - if the output is somehow mangled (ValueError)
|
||||
except (ValueError, FileNotFoundError, OSError,
|
||||
AttributeError, subprocess.CalledProcessError):
|
||||
return size(rows=0, columns=0)
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_advisory(vuln):
|
||||
return vuln.advisory if vuln.advisory else "No advisory found for this vulnerability."
|
||||
class FormatterAPI:
|
||||
"""
|
||||
Strategy Abstract class, with all the render methods that the concrete implementations should support
|
||||
"""
|
||||
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def render_vulnerabilities(self, announcements, vulnerabilities, remediations, full, packages):
|
||||
raise NotImplementedError(NOT_IMPLEMENTED) # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def render_licenses(self, announcements, licenses):
|
||||
raise NotImplementedError(NOT_IMPLEMENTED) # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def render_announcements(self, announcements):
|
||||
raise NotImplementedError(NOT_IMPLEMENTED) # pragma: no cover
|
||||
|
||||
|
||||
class SheetReport(object):
|
||||
REPORT_BANNER = r"""
|
||||
+==============================================================================+
|
||||
| |
|
||||
| /$$$$$$ /$$ |
|
||||
| /$$__ $$ | $$ |
|
||||
| /$$$$$$$ /$$$$$$ | $$ \__//$$$$$$ /$$$$$$ /$$ /$$ |
|
||||
| /$$_____/ |____ $$| $$$$ /$$__ $$|_ $$_/ | $$ | $$ |
|
||||
| | $$$$$$ /$$$$$$$| $$_/ | $$$$$$$$ | $$ | $$ | $$ |
|
||||
| \____ $$ /$$__ $$| $$ | $$_____/ | $$ /$$| $$ | $$ |
|
||||
| /$$$$$$$/| $$$$$$$| $$ | $$$$$$$ | $$$$/| $$$$$$$ |
|
||||
| |_______/ \_______/|__/ \_______/ \___/ \____ $$ |
|
||||
| /$$ | $$ |
|
||||
| | $$$$$$/ |
|
||||
| by pyup.io \______/ |
|
||||
| |
|
||||
+==============================================================================+
|
||||
""".strip()
|
||||
class SafetyFormatter(FormatterAPI):
|
||||
|
||||
TABLE_HEADING = r"""
|
||||
+============================+===========+==========================+==========+
|
||||
| package | installed | affected | ID |
|
||||
+============================+===========+==========================+==========+
|
||||
""".strip()
|
||||
def render_vulnerabilities(self, announcements, vulnerabilities, remediations, full, packages):
|
||||
LOG.info('Safety is going to render_vulnerabilities with format: %s', self.format)
|
||||
return self.format.render_vulnerabilities(announcements, vulnerabilities, remediations, full, packages)
|
||||
|
||||
TABLE_HEADING_LICENSES = r"""
|
||||
+=============================================+===========+====================+
|
||||
| package | version | license |
|
||||
+=============================================+===========+====================+
|
||||
""".strip()
|
||||
def render_licenses(self, announcements, licenses):
|
||||
LOG.info('Safety is going to render_licenses with format: %s', self.format)
|
||||
return self.format.render_licenses(announcements, licenses)
|
||||
|
||||
REPORT_HEADING = r"""
|
||||
| REPORT |
|
||||
""".strip()
|
||||
def render_announcements(self, announcements):
|
||||
LOG.info('Safety is going to render_announcements with format: %s', self.format)
|
||||
return self.format.render_announcements(announcements)
|
||||
|
||||
REPORT_SECTION = r"""
|
||||
+==============================================================================+
|
||||
""".strip()
|
||||
def __init__(self, output):
|
||||
from pipenv.patched.safety.formatters.screen import ScreenReport
|
||||
from pipenv.patched.safety.formatters.text import TextReport
|
||||
from pipenv.patched.safety.formatters.json import JsonReport
|
||||
from pipenv.patched.safety.formatters.bare import BareReport
|
||||
|
||||
REPORT_FOOTER = r"""
|
||||
+==============================================================================+
|
||||
""".strip()
|
||||
self.format = ScreenReport()
|
||||
|
||||
@staticmethod
|
||||
def render(vulns, full, checked_packages, used_db):
|
||||
db_format_str = '{: <' + str(51 - len(str(checked_packages))) + '}'
|
||||
status = "| checked {packages} packages, using {db} |".format(
|
||||
packages=checked_packages,
|
||||
db=db_format_str.format(used_db),
|
||||
section=SheetReport.REPORT_SECTION
|
||||
)
|
||||
if vulns:
|
||||
table = []
|
||||
for n, vuln in enumerate(vulns):
|
||||
table.append("| {:26} | {:9} | {:24} | {:8} |".format(
|
||||
vuln.name[:26],
|
||||
vuln.version[:9],
|
||||
vuln.spec[:24],
|
||||
vuln.vuln_id
|
||||
))
|
||||
if full:
|
||||
table.append(SheetReport.REPORT_SECTION)
|
||||
|
||||
if vuln.cvssv2 is not None:
|
||||
base_score = vuln.cvssv2.get("base_score", "None")
|
||||
impact_score = vuln.cvssv2.get("impact_score", "None")
|
||||
|
||||
table.append("| {:76} |".format(
|
||||
"CVSS v2 | BASE SCORE: {} | IMPACT SCORE: {}".format(
|
||||
base_score,
|
||||
impact_score,
|
||||
)
|
||||
))
|
||||
table.append(SheetReport.REPORT_SECTION)
|
||||
|
||||
if vuln.cvssv3 is not None:
|
||||
base_score = vuln.cvssv3.get("base_score", "None")
|
||||
impact_score = vuln.cvssv3.get("impact_score", "None")
|
||||
base_severity = vuln.cvssv3.get("base_severity", "None")
|
||||
|
||||
table.append("| {:76} |".format(
|
||||
"CVSS v3 | BASE SCORE: {} | IMPACT SCORE: {} | BASE SEVERITY: {}".format(
|
||||
base_score,
|
||||
impact_score,
|
||||
base_severity,
|
||||
)
|
||||
))
|
||||
table.append(SheetReport.REPORT_SECTION)
|
||||
|
||||
advisory_lines = get_advisory(vuln).replace(
|
||||
'\r', ''
|
||||
).splitlines()
|
||||
|
||||
for line in advisory_lines:
|
||||
if line == '':
|
||||
table.append("| {:76} |".format(" "))
|
||||
for wrapped_line in textwrap.wrap(line, width=76):
|
||||
try:
|
||||
table.append("| {:76} |".format(
|
||||
wrapped_line.encode('utf-8')
|
||||
))
|
||||
except TypeError:
|
||||
table.append("| {:76} |".format(
|
||||
wrapped_line
|
||||
))
|
||||
# append the REPORT_SECTION only if this isn't the last entry
|
||||
if n + 1 < len(vulns):
|
||||
table.append(SheetReport.REPORT_SECTION)
|
||||
return "\n".join(
|
||||
[SheetReport.REPORT_BANNER, SheetReport.REPORT_HEADING, status, SheetReport.TABLE_HEADING,
|
||||
"\n".join(table), SheetReport.REPORT_FOOTER]
|
||||
)
|
||||
else:
|
||||
content = "| {:76} |".format("No known security vulnerabilities found.")
|
||||
return "\n".join(
|
||||
[SheetReport.REPORT_BANNER, SheetReport.REPORT_HEADING, status, SheetReport.REPORT_SECTION,
|
||||
content, SheetReport.REPORT_FOOTER]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def render_licenses(packages, packages_licenses):
|
||||
heading = SheetReport.REPORT_HEADING.replace(" ", "", 12).replace(
|
||||
"REPORT", " Packages licenses"
|
||||
)
|
||||
if not packages_licenses:
|
||||
content = "| {:76} |".format("No packages licenses found.")
|
||||
return "\n".join(
|
||||
[SheetReport.REPORT_BANNER, heading, SheetReport.REPORT_SECTION,
|
||||
content, SheetReport.REPORT_FOOTER]
|
||||
)
|
||||
|
||||
table = []
|
||||
iteration = 1
|
||||
for pkg_license in packages_licenses:
|
||||
max_char = last_char = 43 # defines a limit for package name.
|
||||
current_line = 1
|
||||
package = pkg_license['package']
|
||||
license = pkg_license['license']
|
||||
version = pkg_license['version']
|
||||
license_line = int(int(len(package) / max_char) / 2) + 1 # Calc to get which line to add the license info.
|
||||
|
||||
table.append("| {:43} | {:9} | {:18} |".format(
|
||||
package[:max_char],
|
||||
version[:9] if current_line == license_line else "",
|
||||
license[:18] if current_line == license_line else "",
|
||||
))
|
||||
|
||||
long_name = True if len(package[max_char:]) > 0 else False
|
||||
while long_name: # If the package has a long name, break it into multiple lines.
|
||||
current_line += 1
|
||||
table.append("| {:43} | {:9} | {:18} |".format(
|
||||
package[last_char:last_char+max_char],
|
||||
version[:9] if current_line == license_line else "",
|
||||
license[:18] if current_line == license_line else "",
|
||||
))
|
||||
last_char = last_char+max_char
|
||||
long_name = True if len(package[last_char:]) > 0 else False
|
||||
|
||||
if iteration != len(packages_licenses): # Do not add dashes "----" for last package.
|
||||
table.append("|" + ("-" * 78) + "|")
|
||||
iteration += 1
|
||||
return "\n".join(
|
||||
[SheetReport.REPORT_BANNER, heading, SheetReport.TABLE_HEADING_LICENSES,
|
||||
"\n".join(table), SheetReport.REPORT_FOOTER]
|
||||
)
|
||||
|
||||
class BasicReport(object):
|
||||
"""Basic report, intented to be used for terminals with < 80 columns"""
|
||||
|
||||
@staticmethod
|
||||
def render(vulns, full, checked_packages, used_db):
|
||||
table = [
|
||||
"safety report",
|
||||
"checked {packages} packages, using {db}".format(
|
||||
packages=checked_packages,
|
||||
db=used_db
|
||||
),
|
||||
"---"
|
||||
]
|
||||
if vulns:
|
||||
|
||||
for vuln in vulns:
|
||||
table.append("-> {}, installed {}, affected {}, id {}".format(
|
||||
vuln.name,
|
||||
vuln.version[:13],
|
||||
vuln.spec[:27],
|
||||
vuln.vuln_id
|
||||
))
|
||||
if full:
|
||||
if vuln.cvssv2 is not None:
|
||||
base_score = vuln.cvssv2.get("base_score", "None")
|
||||
impact_score = vuln.cvssv2.get("impact_score", "None")
|
||||
|
||||
table.append("CVSS v2 -- BASE SCORE: {}, IMPACT SCORE: {}".format(
|
||||
base_score,
|
||||
impact_score,
|
||||
))
|
||||
|
||||
if vuln.cvssv3 is not None:
|
||||
base_score = vuln.cvssv3.get("base_score", "None")
|
||||
impact_score = vuln.cvssv3.get("impact_score", "None")
|
||||
base_severity = vuln.cvssv3.get("base_severity", "None")
|
||||
|
||||
table.append("CVSS v3 -- BASE SCORE: {}, IMPACT SCORE: {}, BASE SEVERITY: {}".format(
|
||||
base_score,
|
||||
impact_score,
|
||||
base_severity,
|
||||
))
|
||||
|
||||
table.append(get_advisory(vuln))
|
||||
table.append("--")
|
||||
else:
|
||||
table.append("No known security vulnerabilities found.")
|
||||
return "\n".join(
|
||||
table
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def render_licenses(packages, packages_licenses):
|
||||
table = [
|
||||
"safety",
|
||||
"packages licenses",
|
||||
"---"
|
||||
]
|
||||
if not packages_licenses:
|
||||
table.append("No packages licenses found.")
|
||||
return "\n".join(table)
|
||||
|
||||
for pkg_license in packages_licenses:
|
||||
text = pkg_license['package'] + \
|
||||
", version " + pkg_license['version'] + \
|
||||
", license " + pkg_license['license'] + "\n"
|
||||
table.append(text)
|
||||
|
||||
return "\n".join(table)
|
||||
|
||||
class JsonReport(object):
|
||||
"""Json report, for when the output is input for something else"""
|
||||
|
||||
@staticmethod
|
||||
def render(vulns, full):
|
||||
return json.dumps(vulns, indent=4, sort_keys=True)
|
||||
|
||||
@staticmethod
|
||||
def render_licenses(packages_licenses):
|
||||
return json.dumps(packages_licenses, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
class BareReport(object):
|
||||
"""Bare report, for command line tools"""
|
||||
@staticmethod
|
||||
def render(vulns, full):
|
||||
return " ".join(set([v.name for v in vulns]))
|
||||
|
||||
@staticmethod
|
||||
def render_licenses(packages_licenses):
|
||||
licenses = set([pkg_li.get('license') for pkg_li in packages_licenses])
|
||||
if "N/A" in licenses:
|
||||
licenses.remove("N/A")
|
||||
sorted_licenses = sorted(licenses)
|
||||
return " ".join(sorted_licenses)
|
||||
|
||||
|
||||
def get_used_db(key, db):
|
||||
key = key if key else os.environ.get("SAFETY_API_KEY", False)
|
||||
if db:
|
||||
return "local DB"
|
||||
if key:
|
||||
return "pyup.io's DB"
|
||||
return "free DB (updated once a month)"
|
||||
|
||||
|
||||
def report(vulns, full=False, json_report=False, bare_report=False, checked_packages=0, db=None, key=None):
|
||||
if bare_report:
|
||||
return BareReport.render(vulns, full=full)
|
||||
if json_report:
|
||||
return JsonReport.render(vulns, full=full)
|
||||
size = get_terminal_size()
|
||||
used_db = get_used_db(key=key, db=db)
|
||||
if size.columns >= 80:
|
||||
return SheetReport.render(vulns, full=full, checked_packages=checked_packages, used_db=used_db)
|
||||
return BasicReport.render(vulns, full=full, checked_packages=checked_packages, used_db=used_db)
|
||||
|
||||
|
||||
def license_report(packages, licenses, json_report=False, bare_report=False):
|
||||
if json_report:
|
||||
return JsonReport.render_licenses(packages_licenses=licenses)
|
||||
elif bare_report:
|
||||
return BareReport.render_licenses(packages_licenses=licenses)
|
||||
|
||||
size = get_terminal_size()
|
||||
if size.columns >= 80:
|
||||
return SheetReport.render_licenses(packages, licenses)
|
||||
return BasicReport.render_licenses(packages, licenses)
|
||||
if output == 'json':
|
||||
self.format = JsonReport()
|
||||
elif output == 'bare':
|
||||
self.format = BareReport()
|
||||
elif output == 'text':
|
||||
self.format = TextReport()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from pipenv.patched.safety.formatter import FormatterAPI
|
||||
from pipenv.patched.safety.util import get_basic_announcements
|
||||
|
||||
|
||||
class BareReport(FormatterAPI):
|
||||
"""Bare report, for command line tools"""
|
||||
|
||||
def render_vulnerabilities(self, announcements, vulnerabilities, remediations, full, packages):
|
||||
parsed_announcements = []
|
||||
|
||||
Announcement = namedtuple("Announcement", ["name"])
|
||||
|
||||
for announcement in get_basic_announcements(announcements):
|
||||
normalized_message = "-".join(announcement.get('message', 'none').lower().split())
|
||||
parsed_announcements.append(Announcement(name=normalized_message))
|
||||
|
||||
announcements_to_render = [announcement.name for announcement in parsed_announcements]
|
||||
affected_packages = list(set([v.package_name for v in vulnerabilities if not v.ignored]))
|
||||
|
||||
return " ".join(announcements_to_render + affected_packages)
|
||||
|
||||
def render_licenses(self, announcements, packages_licenses):
|
||||
parsed_announcements = []
|
||||
|
||||
for announcement in get_basic_announcements(announcements):
|
||||
normalized_message = "-".join(announcement.get('message', 'none').lower().split())
|
||||
parsed_announcements.append({'license': normalized_message})
|
||||
|
||||
announcements_to_render = [announcement.get('license') for announcement in parsed_announcements]
|
||||
|
||||
licenses = list(set([pkg_li.get('license') for pkg_li in packages_licenses]))
|
||||
sorted_licenses = sorted(licenses)
|
||||
return " ".join(announcements_to_render + sorted_licenses)
|
||||
|
||||
def render_announcements(self, announcements):
|
||||
print('render_announcements bare')
|
||||
@@ -0,0 +1,84 @@
|
||||
import logging
|
||||
|
||||
import json as json_parser
|
||||
|
||||
from pipenv.patched.pip._vendor.requests.models import PreparedRequest
|
||||
|
||||
from pipenv.patched.safety.formatter import FormatterAPI
|
||||
from pipenv.patched.safety.output_utils import get_report_brief_info
|
||||
from pipenv.patched.safety.util import get_basic_announcements
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JsonReport(FormatterAPI):
|
||||
"""Json report, for when the output is input for something else"""
|
||||
|
||||
def render_vulnerabilities(self, announcements, vulnerabilities, remediations, full, packages):
|
||||
remediations_recommended = len(remediations.keys())
|
||||
LOG.debug('Rendering %s vulnerabilities, %s remediations with full_report: %s', len(vulnerabilities),
|
||||
remediations_recommended, full)
|
||||
vulns_ignored = [vuln.to_dict() for vuln in vulnerabilities if vuln.ignored]
|
||||
vulns = [vuln.to_dict() for vuln in vulnerabilities if not vuln.ignored]
|
||||
|
||||
report = get_report_brief_info(as_dict=True, report_type=1, vulnerabilities_found=len(vulns),
|
||||
vulnerabilities_ignored=len(vulns_ignored),
|
||||
remediations_recommended=remediations_recommended)
|
||||
|
||||
remed = {}
|
||||
for k, v in remediations.items():
|
||||
if k not in remed:
|
||||
remed[k] = {}
|
||||
|
||||
closest = v.get('closest_secure_version', {})
|
||||
upgrade = closest.get('major', None)
|
||||
downgrade = closest.get('minor', None)
|
||||
|
||||
recommended_version = None
|
||||
|
||||
if upgrade:
|
||||
recommended_version = str(upgrade)
|
||||
elif downgrade:
|
||||
recommended_version = str(downgrade)
|
||||
|
||||
remed[k]['current_version'] = v.get('version', None)
|
||||
remed[k]['vulnerabilities_found'] = v.get('vulns_found', 0)
|
||||
remed[k]['recommended_version'] = recommended_version
|
||||
remed[k]['other_recommended_versions'] = [other_v for other_v in v.get('secure_versions', []) if
|
||||
other_v != recommended_version]
|
||||
remed[k]['more_info_url'] = v.get('more_info_url', '')
|
||||
|
||||
# Use Request's PreparedRequest to handle parsing, joining etc the URL since we're adding query
|
||||
# parameters and don't know what the server might send down.
|
||||
if remed[k]['more_info_url']:
|
||||
req = PreparedRequest()
|
||||
req.prepare_url(remed[k]['more_info_url'], {'from': remed[k]['current_version'], 'to': recommended_version})
|
||||
remed[k]['more_info_url'] = req.url
|
||||
|
||||
template = {
|
||||
"report_meta": report,
|
||||
"scanned_packages": {p.name: p.to_dict(short_version=True) for p in packages},
|
||||
"affected_packages": {v.pkg.name: v.pkg.to_dict() for v in vulnerabilities},
|
||||
"announcements": [{'type': item.get('type'), 'message': item.get('message')} for item in
|
||||
get_basic_announcements(announcements)],
|
||||
"vulnerabilities": vulns,
|
||||
"ignored_vulnerabilities": vulns_ignored,
|
||||
"remediations": remed
|
||||
}
|
||||
|
||||
return json_parser.dumps(template, indent=4)
|
||||
|
||||
def render_licenses(self, announcements, licenses):
|
||||
unique_license_types = set([lic['license'] for lic in licenses])
|
||||
report = get_report_brief_info(as_dict=True, report_type=2, licenses_found=len(unique_license_types))
|
||||
|
||||
template = {
|
||||
"report_meta": report,
|
||||
"announcements": get_basic_announcements(announcements),
|
||||
"licenses": licenses,
|
||||
}
|
||||
|
||||
return json_parser.dumps(template, indent=4)
|
||||
|
||||
def render_announcements(self, announcements):
|
||||
return json_parser.dumps({"announcements": get_basic_announcements(announcements)}, indent=4)
|
||||
@@ -0,0 +1,143 @@
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
from pipenv.patched.safety.formatter import FormatterAPI
|
||||
from pipenv.patched.safety.output_utils import build_announcements_section_content, format_long_text, \
|
||||
add_empty_line, format_vulnerability, get_final_brief, \
|
||||
build_report_brief_section, format_license, get_final_brief_license, build_remediation_section, \
|
||||
build_primary_announcement
|
||||
from pipenv.patched.safety.util import get_primary_announcement, get_basic_announcements, get_terminal_size
|
||||
|
||||
|
||||
class ScreenReport(FormatterAPI):
|
||||
DIVIDER_SECTIONS = '+' + '=' * (get_terminal_size().columns - 2) + '+'
|
||||
|
||||
REPORT_BANNER = DIVIDER_SECTIONS + '\n' + r"""
|
||||
/$$$$$$ /$$
|
||||
/$$__ $$ | $$
|
||||
/$$$$$$$ /$$$$$$ | $$ \__//$$$$$$ /$$$$$$ /$$ /$$
|
||||
/$$_____/ |____ $$| $$$$ /$$__ $$|_ $$_/ | $$ | $$
|
||||
| $$$$$$ /$$$$$$$| $$_/ | $$$$$$$$ | $$ | $$ | $$
|
||||
\____ $$ /$$__ $$| $$ | $$_____/ | $$ /$$| $$ | $$
|
||||
/$$$$$$$/| $$$$$$$| $$ | $$$$$$$ | $$$$/| $$$$$$$
|
||||
|_______/ \_______/|__/ \_______/ \___/ \____ $$
|
||||
/$$ | $$
|
||||
| $$$$$$/
|
||||
by pyup.io \______/
|
||||
|
||||
""" + DIVIDER_SECTIONS
|
||||
|
||||
ANNOUNCEMENTS_HEADING = format_long_text(click.style('ANNOUNCEMENTS', bold=True))
|
||||
|
||||
def __build_announcements_section(self, announcements):
|
||||
announcements_section = []
|
||||
|
||||
basic_announcements = get_basic_announcements(announcements)
|
||||
|
||||
if basic_announcements:
|
||||
announcements_content = build_announcements_section_content(basic_announcements)
|
||||
announcements_section = [add_empty_line(), self.ANNOUNCEMENTS_HEADING, add_empty_line(),
|
||||
announcements_content, add_empty_line(), self.DIVIDER_SECTIONS]
|
||||
|
||||
return announcements_section
|
||||
|
||||
def render_vulnerabilities(self, announcements, vulnerabilities, remediations, full, packages):
|
||||
announcements_section = self.__build_announcements_section(announcements)
|
||||
primary_announcement = get_primary_announcement(announcements)
|
||||
remediation_section = build_remediation_section(remediations)
|
||||
end_content = []
|
||||
|
||||
if primary_announcement:
|
||||
end_content = [add_empty_line(),
|
||||
build_primary_announcement(primary_announcement, columns=get_terminal_size().columns),
|
||||
self.DIVIDER_SECTIONS]
|
||||
|
||||
table = []
|
||||
ignored = {}
|
||||
total_ignored = 0
|
||||
|
||||
for n, vuln in enumerate(vulnerabilities):
|
||||
if vuln.ignored:
|
||||
total_ignored += 1
|
||||
ignored[vuln.package_name] = ignored.get(vuln.package_name, 0) + 1
|
||||
table.append(format_vulnerability(vuln, full))
|
||||
|
||||
report_brief_section = build_report_brief_section(primary_announcement=primary_announcement, report_type=1,
|
||||
vulnerabilities_found=max(0, len(vulnerabilities)-total_ignored),
|
||||
vulnerabilities_ignored=total_ignored,
|
||||
remediations_recommended=len(remediations))
|
||||
|
||||
if vulnerabilities:
|
||||
|
||||
final_brief = get_final_brief(len(vulnerabilities), len(remediations), ignored, total_ignored)
|
||||
|
||||
return "\n".join(
|
||||
[ScreenReport.REPORT_BANNER] + announcements_section + [report_brief_section,
|
||||
add_empty_line(),
|
||||
self.DIVIDER_SECTIONS,
|
||||
format_long_text(
|
||||
click.style('VULNERABILITIES FOUND',
|
||||
bold=True, fg='red')),
|
||||
self.DIVIDER_SECTIONS,
|
||||
add_empty_line(),
|
||||
"\n\n".join(table),
|
||||
final_brief,
|
||||
add_empty_line(),
|
||||
self.DIVIDER_SECTIONS] +
|
||||
remediation_section + end_content
|
||||
)
|
||||
else:
|
||||
content = format_long_text(click.style("No known security vulnerabilities found.", bold=True, fg='green'))
|
||||
return "\n".join(
|
||||
[ScreenReport.REPORT_BANNER] + announcements_section + [report_brief_section,
|
||||
self.DIVIDER_SECTIONS,
|
||||
add_empty_line(),
|
||||
content,
|
||||
add_empty_line(),
|
||||
self.DIVIDER_SECTIONS] +
|
||||
end_content
|
||||
)
|
||||
|
||||
def render_licenses(self, announcements, licenses):
|
||||
unique_license_types = set([lic['license'] for lic in licenses])
|
||||
|
||||
report_brief_section = build_report_brief_section(primary_announcement=get_primary_announcement(announcements),
|
||||
report_type=2, licenses_found=len(unique_license_types))
|
||||
announcements_section = self.__build_announcements_section(announcements)
|
||||
|
||||
if not licenses:
|
||||
content = format_long_text(click.style("No packages licenses found.", bold=True, fg='red'))
|
||||
return "\n".join(
|
||||
[ScreenReport.REPORT_BANNER] + announcements_section + [report_brief_section,
|
||||
self.DIVIDER_SECTIONS,
|
||||
add_empty_line(),
|
||||
content,
|
||||
add_empty_line(),
|
||||
self.DIVIDER_SECTIONS]
|
||||
)
|
||||
|
||||
table = []
|
||||
for license in licenses:
|
||||
table.append(format_license(license))
|
||||
|
||||
final_brief = get_final_brief_license(unique_license_types)
|
||||
|
||||
return "\n".join(
|
||||
[ScreenReport.REPORT_BANNER] + announcements_section + [report_brief_section,
|
||||
add_empty_line(),
|
||||
self.DIVIDER_SECTIONS,
|
||||
format_long_text(
|
||||
click.style('LICENSES FOUND',
|
||||
bold=True, fg='yellow')),
|
||||
self.DIVIDER_SECTIONS,
|
||||
add_empty_line(),
|
||||
"\n".join(table),
|
||||
final_brief,
|
||||
add_empty_line(),
|
||||
self.DIVIDER_SECTIONS]
|
||||
)
|
||||
|
||||
def render_announcements(self, announcements):
|
||||
return self.__build_announcements_section(announcements)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
from pipenv.patched.safety.formatter import FormatterAPI
|
||||
from pipenv.patched.safety.output_utils import build_announcements_section_content, format_vulnerability, \
|
||||
build_report_brief_section, get_final_brief_license, add_empty_line, get_final_brief, build_remediation_section, \
|
||||
build_primary_announcement
|
||||
from pipenv.patched.safety.util import get_primary_announcement, get_basic_announcements
|
||||
|
||||
|
||||
class TextReport(FormatterAPI):
|
||||
"""Basic report, intented to be used for terminals with < 80 columns"""
|
||||
|
||||
SMALL_DIVIDER_SECTIONS = '+' + '=' * 78 + '+'
|
||||
|
||||
TEXT_REPORT_BANNER = SMALL_DIVIDER_SECTIONS + '\n' + r"""
|
||||
/$$$$$$ /$$
|
||||
/$$__ $$ | $$
|
||||
/$$$$$$$ /$$$$$$ | $$ \__//$$$$$$ /$$$$$$ /$$ /$$
|
||||
/$$_____/ |____ $$| $$$$ /$$__ $$|_ $$_/ | $$ | $$
|
||||
| $$$$$$ /$$$$$$$| $$_/ | $$$$$$$$ | $$ | $$ | $$
|
||||
\____ $$ /$$__ $$| $$ | $$_____/ | $$ /$$| $$ | $$
|
||||
/$$$$$$$/| $$$$$$$| $$ | $$$$$$$ | $$$$/| $$$$$$$
|
||||
|_______/ \_______/|__/ \_______/ \___/ \____ $$
|
||||
/$$ | $$
|
||||
| $$$$$$/
|
||||
by pyup.io \______/
|
||||
|
||||
""" + SMALL_DIVIDER_SECTIONS
|
||||
|
||||
def __build_announcements_section(self, announcements):
|
||||
announcements_table = []
|
||||
|
||||
basic_announcements = get_basic_announcements(announcements)
|
||||
|
||||
if basic_announcements:
|
||||
announcements_content = click.unstyle(build_announcements_section_content(basic_announcements,
|
||||
columns=80,
|
||||
start_line_decorator=' ' * 2,
|
||||
end_line_decorator=''))
|
||||
announcements_table = [add_empty_line(), 'ANNOUNCEMENTS', add_empty_line(),
|
||||
announcements_content, add_empty_line(), self.SMALL_DIVIDER_SECTIONS]
|
||||
|
||||
return announcements_table
|
||||
|
||||
def render_vulnerabilities(self, announcements, vulnerabilities, remediations, full, packages):
|
||||
primary_announcement = get_primary_announcement(announcements)
|
||||
remediation_section = [click.unstyle(rem) for rem in build_remediation_section(remediations, columns=80)]
|
||||
end_content = []
|
||||
|
||||
if primary_announcement:
|
||||
end_content = [add_empty_line(),
|
||||
build_primary_announcement(primary_announcement, columns=80, only_text=True),
|
||||
self.SMALL_DIVIDER_SECTIONS]
|
||||
|
||||
announcement_section = self.__build_announcements_section(announcements)
|
||||
|
||||
ignored = {}
|
||||
total_ignored = 0
|
||||
|
||||
for n, vuln in enumerate(vulnerabilities):
|
||||
if vuln.ignored:
|
||||
total_ignored += 1
|
||||
ignored[vuln.package_name] = ignored.get(vuln.package_name, 0) + 1
|
||||
|
||||
report_brief_section = click.unstyle(
|
||||
build_report_brief_section(columns=80, primary_announcement=primary_announcement,
|
||||
vulnerabilities_found=max(0, len(vulnerabilities)-total_ignored),
|
||||
vulnerabilities_ignored=total_ignored,
|
||||
remediations_recommended=len(remediations)))
|
||||
|
||||
table = [self.TEXT_REPORT_BANNER] + announcement_section + [
|
||||
report_brief_section,
|
||||
'',
|
||||
self.SMALL_DIVIDER_SECTIONS,
|
||||
]
|
||||
|
||||
if vulnerabilities:
|
||||
table += [" VULNERABILITIES FOUND", self.SMALL_DIVIDER_SECTIONS]
|
||||
|
||||
for vuln in vulnerabilities:
|
||||
table.append('\n' + format_vulnerability(vuln, full, only_text=True, columns=80))
|
||||
|
||||
final_brief = click.unstyle(get_final_brief(len(vulnerabilities), len(remediations), ignored, total_ignored,
|
||||
kwargs={'columns': 80}))
|
||||
table += [final_brief, add_empty_line(), self.SMALL_DIVIDER_SECTIONS] + remediation_section + end_content
|
||||
|
||||
else:
|
||||
table += [add_empty_line(), " No known security vulnerabilities found.", add_empty_line(),
|
||||
self.SMALL_DIVIDER_SECTIONS] + end_content
|
||||
|
||||
return "\n".join(
|
||||
table
|
||||
)
|
||||
|
||||
def render_licenses(self, announcements, licenses):
|
||||
unique_license_types = set([lic['license'] for lic in licenses])
|
||||
|
||||
report_brief_section = click.unstyle(
|
||||
build_report_brief_section(columns=80, primary_announcement=get_primary_announcement(announcements),
|
||||
licenses_found=len(unique_license_types)))
|
||||
|
||||
packages_licenses = licenses
|
||||
announcements_table = self.__build_announcements_section(announcements)
|
||||
|
||||
final_brief = click.unstyle(
|
||||
get_final_brief_license(unique_license_types, kwargs={'columns': 80}))
|
||||
|
||||
table = [self.TEXT_REPORT_BANNER] + announcements_table + [
|
||||
report_brief_section,
|
||||
self.SMALL_DIVIDER_SECTIONS,
|
||||
" LICENSES",
|
||||
self.SMALL_DIVIDER_SECTIONS,
|
||||
add_empty_line(),
|
||||
]
|
||||
|
||||
if not packages_licenses:
|
||||
table.append(" No packages licenses found.")
|
||||
table += [final_brief, add_empty_line(), self.SMALL_DIVIDER_SECTIONS]
|
||||
|
||||
return "\n".join(table)
|
||||
|
||||
for pkg_license in packages_licenses:
|
||||
text = " {0}, version {1}, license {2}\n".format(pkg_license['package'], pkg_license['version'],
|
||||
pkg_license['license'])
|
||||
table.append(text)
|
||||
|
||||
table += [final_brief, add_empty_line(), self.SMALL_DIVIDER_SECTIONS]
|
||||
|
||||
return "\n".join(table)
|
||||
|
||||
def render_announcements(self, announcements):
|
||||
rows = self.__build_announcements_section(announcements)
|
||||
rows.insert(0, self.SMALL_DIVIDER_SECTIONS)
|
||||
return '\n'.join(rows)
|
||||
@@ -0,0 +1,110 @@
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class DictConverter(object):
|
||||
|
||||
def to_dict(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
announcement_nmt = namedtuple('Announcement', ['type', 'message'])
|
||||
remediation_nmt = namedtuple('Remediation', ['Package', 'closest_secure_version', 'secure_versions',
|
||||
'latest_package_version'])
|
||||
cve_nmt = namedtuple('Cve', ['name', 'cvssv2', 'cvssv3'])
|
||||
severity_nmt = namedtuple('Severity', ['source', 'cvssv2', 'cvssv3'])
|
||||
vulnerability_nmt = namedtuple('Vulnerability',
|
||||
['vulnerability_id', 'package_name', 'pkg', 'ignored', 'ignored_reason', 'ignored_expires',
|
||||
'vulnerable_spec', 'all_vulnerable_specs', 'analyzed_version', 'advisory',
|
||||
'is_transitive', 'published_date', 'fixed_versions',
|
||||
'closest_versions_without_known_vulnerabilities', 'resources', 'CVE', 'severity',
|
||||
'affected_versions', 'more_info_url'])
|
||||
package_nmt = namedtuple('Package', ['name', 'version', 'found', 'insecure_versions', 'secure_versions',
|
||||
'latest_version_without_known_vulnerabilities', 'latest_version', 'more_info_url'])
|
||||
package_nmt.__new__.__defaults__ = (None,) * len(package_nmt._fields) # Ugly hack for now
|
||||
RequirementFile = namedtuple('RequirementFile', ['path'])
|
||||
|
||||
|
||||
class Package(package_nmt, DictConverter):
|
||||
|
||||
def to_dict(self, **kwargs):
|
||||
if kwargs.get('short_version', False):
|
||||
return {
|
||||
'name': self.name,
|
||||
'version': self.version,
|
||||
}
|
||||
|
||||
return {'name': self.name,
|
||||
'version': self.version,
|
||||
'found': self.found,
|
||||
'insecure_versions': self.insecure_versions,
|
||||
'secure_versions': self.secure_versions,
|
||||
'latest_version_without_known_vulnerabilities': self.latest_version_without_known_vulnerabilities,
|
||||
'latest_version': self.latest_version,
|
||||
'more_info_url': self.more_info_url
|
||||
}
|
||||
|
||||
|
||||
class Announcement(announcement_nmt):
|
||||
pass
|
||||
|
||||
|
||||
class Remediation(remediation_nmt, DictConverter):
|
||||
|
||||
def to_dict(self):
|
||||
return {'package': self.Package.name,
|
||||
'closest_secure_version': self.closest_secure_version,
|
||||
'secure_versions': self.secure_versions,
|
||||
'latest_package_version': self.latest_package_version
|
||||
}
|
||||
|
||||
|
||||
class CVE(cve_nmt, DictConverter):
|
||||
|
||||
def to_dict(self):
|
||||
return {'name': self.name, 'cvssv2': self.cvssv2, 'cvssv3': self.cvssv3}
|
||||
|
||||
|
||||
class Severity(severity_nmt, DictConverter):
|
||||
def to_dict(self):
|
||||
result = {'severity': {'source': self.source}}
|
||||
|
||||
result['severity']['cvssv2'] = self.cvssv2
|
||||
result['severity']['cvssv3'] = self.cvssv3
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Vulnerability(vulnerability_nmt):
|
||||
|
||||
def to_dict(self):
|
||||
empty_list_if_none = ['fixed_versions', 'closest_versions_without_known_vulnerabilities', 'resources']
|
||||
result = {
|
||||
}
|
||||
|
||||
ignore = ['pkg']
|
||||
|
||||
for field, value in zip(self._fields, self):
|
||||
if field in ignore:
|
||||
continue
|
||||
|
||||
if value is None and field in empty_list_if_none:
|
||||
value = []
|
||||
|
||||
if isinstance(value, CVE):
|
||||
val = None
|
||||
if value.name.startswith("CVE"):
|
||||
val = value.name
|
||||
result[field] = val
|
||||
elif isinstance(value, DictConverter):
|
||||
result.update(value.to_dict())
|
||||
elif isinstance(value, datetime):
|
||||
result[field] = str(value)
|
||||
else:
|
||||
result[field] = value
|
||||
|
||||
return result
|
||||
|
||||
def get_advisory(self):
|
||||
return self.advisory.replace('\r', '') if self.advisory else "No advisory found for this vulnerability."
|
||||
@@ -0,0 +1,693 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
from pipenv.patched.safety.constants import RED, YELLOW
|
||||
from pipenv.patched.safety.util import get_safety_version, Package, get_terminal_size, \
|
||||
SafetyContext, build_telemetry_data, build_git_data, is_a_remote_mirror
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_announcements_section_content(announcements, columns=get_terminal_size().columns,
|
||||
start_line_decorator=' ', end_line_decorator=' '):
|
||||
section = ''
|
||||
|
||||
for i, announcement in enumerate(announcements):
|
||||
|
||||
color = ''
|
||||
if announcement.get('type') == 'error':
|
||||
color = RED
|
||||
elif announcement.get('type') == 'warning':
|
||||
color = YELLOW
|
||||
|
||||
item = '{message}'.format(
|
||||
message=format_long_text('* ' + announcement.get('message'), color, columns,
|
||||
start_line_decorator, end_line_decorator))
|
||||
section += '{item}'.format(item=item)
|
||||
|
||||
if i + 1 < len(announcements):
|
||||
section += '\n'
|
||||
|
||||
return section
|
||||
|
||||
|
||||
def add_empty_line():
|
||||
return format_long_text('')
|
||||
|
||||
|
||||
def style_lines(lines, columns, pre_processed_text='', start_line=' ' * 4, end_line=' ' * 4):
|
||||
styled_text = pre_processed_text
|
||||
|
||||
for line in lines:
|
||||
styled_line = ''
|
||||
left_padding = ' ' * line.get('left_padding', 0)
|
||||
|
||||
for i, word in enumerate(line.get('words', [])):
|
||||
if word.get('style', {}):
|
||||
text = ''
|
||||
|
||||
if i == 0:
|
||||
text = left_padding # Include the line padding in the word to avoid Github issues
|
||||
left_padding = '' # Clean left padding to avoid be added two times
|
||||
|
||||
text += word.get('value', '')
|
||||
|
||||
styled_line += click.style(text=text, **word.get('style', {}))
|
||||
else:
|
||||
styled_line += word.get('value', '')
|
||||
|
||||
styled_text += format_long_text(styled_line, columns=columns, start_line_decorator=start_line,
|
||||
end_line_decorator=end_line,
|
||||
left_padding=left_padding, **line.get('format', {})) + '\n'
|
||||
|
||||
return styled_text
|
||||
|
||||
|
||||
def format_vulnerability(vulnerability, full_mode, only_text=False, columns=get_terminal_size().columns):
|
||||
|
||||
common_format = {'left_padding': 3, 'format': {'sub_indent': ' ' * 3, 'max_lines': None}}
|
||||
|
||||
styled_vulnerability = [
|
||||
{'words': [{'style': {'bold': True}, 'value': 'Vulnerability ID: '}, {'value': vulnerability.vulnerability_id}]},
|
||||
]
|
||||
|
||||
vulnerability_spec = [
|
||||
{'words': [{'style': {'bold': True}, 'value': 'Affected spec: '}, {'value': vulnerability.vulnerable_spec}]}]
|
||||
|
||||
cve = vulnerability.CVE
|
||||
|
||||
cvssv2_line = None
|
||||
cve_lines = []
|
||||
|
||||
if cve:
|
||||
if full_mode and cve.cvssv2:
|
||||
b = cve.cvssv2.get("base_score", "-")
|
||||
s = cve.cvssv2.get("impact_score", "-")
|
||||
v = cve.cvssv2.get("vector_string", "-")
|
||||
|
||||
# Reset sub_indent as the left_margin is going to be applied in this case
|
||||
cvssv2_line = {'format': {'sub_indent': ''}, 'words': [
|
||||
{'value': f'CVSS v2, BASE SCORE {b}, IMPACT SCORE {s}, VECTOR STRING {v}'},
|
||||
]}
|
||||
|
||||
if cve.cvssv3 and "base_severity" in cve.cvssv3.keys():
|
||||
cvss_base_severity_style = {'bold': True}
|
||||
base_severity = cve.cvssv3.get("base_severity", "-")
|
||||
|
||||
if base_severity.upper() in ['HIGH', 'CRITICAL']:
|
||||
cvss_base_severity_style['fg'] = 'red'
|
||||
|
||||
b = cve.cvssv3.get("base_score", "-")
|
||||
|
||||
if full_mode:
|
||||
s = cve.cvssv3.get("impact_score", "-")
|
||||
v = cve.cvssv3.get("vector_string", "-")
|
||||
|
||||
cvssv3_text = f'CVSS v3, BASE SCORE {b}, IMPACT SCORE {s}, VECTOR STRING {v}'
|
||||
|
||||
else:
|
||||
cvssv3_text = f'CVSS v3, BASE SCORE {b} '
|
||||
|
||||
cve_lines = [
|
||||
{'words': [{'style': {'bold': True}, 'value': '{0} is '.format(cve.name)},
|
||||
{'style': cvss_base_severity_style,
|
||||
'value': f'{base_severity} SEVERITY => '},
|
||||
{'value': cvssv3_text},
|
||||
]},
|
||||
]
|
||||
|
||||
if cvssv2_line:
|
||||
cve_lines.append(cvssv2_line)
|
||||
|
||||
elif cve.name:
|
||||
cve_lines = [
|
||||
{'words': [{'style': {'bold': True}, 'value': cve.name}]}
|
||||
]
|
||||
|
||||
advisory_format = {'sub_indent': ' ' * 3, 'max_lines': None} if full_mode else {'sub_indent': ' ' * 3,
|
||||
'max_lines': 2}
|
||||
|
||||
basic_vuln_data_lines = [
|
||||
{'format': advisory_format, 'words': [
|
||||
{'style': {'bold': True}, 'value': 'ADVISORY: '},
|
||||
{'value': vulnerability.advisory.replace('\n', '')}]}
|
||||
]
|
||||
|
||||
if SafetyContext().key:
|
||||
fixed_version_line = {'words': [
|
||||
{'style': {'bold': True}, 'value': 'Fixed versions: '},
|
||||
{'value': ', '.join(vulnerability.fixed_versions) if vulnerability.fixed_versions else 'No known fix'}
|
||||
]}
|
||||
|
||||
basic_vuln_data_lines.append(fixed_version_line)
|
||||
|
||||
more_info_line = [{'words': [{'style': {'bold': True}, 'value': 'For more information, please visit '},
|
||||
{'value': click.style(vulnerability.more_info_url)}]}]
|
||||
|
||||
vuln_title = f'-> Vulnerability found in {vulnerability.package_name} version {vulnerability.analyzed_version}\n'
|
||||
|
||||
styled_text = click.style(vuln_title, fg='red')
|
||||
|
||||
to_print = styled_vulnerability
|
||||
|
||||
if not vulnerability.ignored:
|
||||
to_print += vulnerability_spec + basic_vuln_data_lines + cve_lines
|
||||
else:
|
||||
generic_reason = 'This vulnerability is being ignored'
|
||||
if vulnerability.ignored_expires:
|
||||
generic_reason += f" until {vulnerability.ignored_expires.strftime('%Y-%m-%d %H:%M:%S UTC')}. " \
|
||||
f"See your configurations"
|
||||
|
||||
specific_reason = None
|
||||
if vulnerability.ignored_reason:
|
||||
specific_reason = [
|
||||
{'words': [{'style': {'bold': True}, 'value': 'Reason: '}, {'value': vulnerability.ignored_reason}]}]
|
||||
|
||||
expire_section = [{'words': [
|
||||
{'style': {'bold': True, 'fg': 'green'}, 'value': f'{generic_reason}.'}, ]}]
|
||||
|
||||
if specific_reason:
|
||||
expire_section += specific_reason
|
||||
|
||||
to_print += expire_section
|
||||
|
||||
if cve:
|
||||
to_print += more_info_line
|
||||
|
||||
to_print = [{**common_format, **line} for line in to_print]
|
||||
|
||||
content = style_lines(to_print, columns, styled_text, start_line='', end_line='', )
|
||||
|
||||
return click.unstyle(content) if only_text else content
|
||||
|
||||
|
||||
def format_license(license, only_text=False, columns=get_terminal_size().columns):
|
||||
to_print = [
|
||||
{'words': [{'style': {'bold': True}, 'value': license['package']},
|
||||
{'value': ' version {0} found using license '.format(license['version'])},
|
||||
{'style': {'bold': True}, 'value': license['license']}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
content = style_lines(to_print, columns, '-> ', start_line='', end_line='')
|
||||
|
||||
return click.unstyle(content) if only_text else content
|
||||
|
||||
|
||||
def build_remediation_section(remediations, only_text=False, columns=get_terminal_size().columns, kwargs=None):
|
||||
columns -= 2
|
||||
left_padding = ' ' * 3
|
||||
|
||||
if not kwargs:
|
||||
# Reset default params in the format_long_text func
|
||||
kwargs = {'left_padding': '', 'columns': columns, 'start_line_decorator': '', 'end_line_decorator': '',
|
||||
'sub_indent': left_padding}
|
||||
|
||||
END_SECTION = '+' + '=' * columns + '+'
|
||||
|
||||
if not remediations:
|
||||
return []
|
||||
|
||||
content = ''
|
||||
total_vulns = 0
|
||||
total_packages = len(remediations.keys())
|
||||
|
||||
for pkg in remediations.keys():
|
||||
total_vulns += remediations[pkg]['vulns_found']
|
||||
upgrade_to = remediations[pkg]['closest_secure_version']['major']
|
||||
downgrade_to = remediations[pkg]['closest_secure_version']['minor']
|
||||
fix_version = None
|
||||
|
||||
if upgrade_to:
|
||||
fix_version = str(upgrade_to)
|
||||
elif downgrade_to:
|
||||
fix_version = str(downgrade_to)
|
||||
|
||||
new_line = '\n'
|
||||
|
||||
other_options = [str(fix) for fix in remediations[pkg].get('secure_versions', []) if str(fix) != fix_version]
|
||||
raw_recommendation = f"We recommend upgrading to version {upgrade_to} of {pkg}."
|
||||
|
||||
if other_options:
|
||||
raw_other_options = ', '.join(other_options)
|
||||
raw_pre_other_options = 'Other versions without known vulnerabilities are:'
|
||||
if len(other_options) == 1:
|
||||
raw_pre_other_options = 'Other version without known vulnerabilities is'
|
||||
raw_recommendation = f"{raw_recommendation} {raw_pre_other_options} " \
|
||||
f"{raw_other_options}"
|
||||
|
||||
remediation_content = [
|
||||
f'{left_padding}The closest version with no known vulnerabilities is ' + click.style(upgrade_to, bold=True),
|
||||
new_line,
|
||||
click.style(f'{left_padding}{raw_recommendation}', bold=True, fg='green')
|
||||
]
|
||||
|
||||
if not fix_version:
|
||||
remediation_content = [new_line,
|
||||
click.style(f'{left_padding}There is no known fix for this vulnerability.', bold=True, fg='yellow')]
|
||||
|
||||
text = 'vulnerabilities' if remediations[pkg]['vulns_found'] > 1 else 'vulnerability'
|
||||
|
||||
raw_rem_title = f"-> {pkg} version {remediations[pkg]['version']} was found, " \
|
||||
f"which has {remediations[pkg]['vulns_found']} {text}"
|
||||
|
||||
remediation_title = click.style(raw_rem_title, fg=RED, bold=True)
|
||||
|
||||
content += new_line + format_long_text(remediation_title, **kwargs) + new_line
|
||||
|
||||
pre_content = remediation_content + [
|
||||
f"{left_padding}For more information, please visit {remediations[pkg]['more_info_url']}",
|
||||
f'{left_padding}Always check for breaking changes when upgrading packages.',
|
||||
new_line]
|
||||
|
||||
for i, element in enumerate(pre_content):
|
||||
content += format_long_text(element, **kwargs)
|
||||
|
||||
if i + 1 < len(pre_content):
|
||||
content += '\n'
|
||||
|
||||
title = format_long_text(click.style(f'{left_padding}REMEDIATIONS', fg='green', bold=True), **kwargs)
|
||||
|
||||
body = [content]
|
||||
|
||||
if not is_using_api_key():
|
||||
vuln_text = 'vulnerabilities were' if total_vulns != 1 else 'vulnerability was'
|
||||
pkg_text = 'packages' if total_packages > 1 else 'package'
|
||||
msg = "{0} {1} found in {2} {3}. " \
|
||||
"For detailed remediation & fix recommendations, upgrade to a commercial license."\
|
||||
.format(total_vulns, vuln_text, total_packages, pkg_text)
|
||||
content = '\n' + format_long_text(msg, left_padding=' ', columns=columns) + '\n'
|
||||
body = [content]
|
||||
|
||||
body.append(END_SECTION)
|
||||
|
||||
content = [title] + body
|
||||
|
||||
if only_text:
|
||||
content = [click.unstyle(item) for item in content]
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def get_final_brief(total_vulns_found, total_remediations, ignored, total_ignored, kwargs=None):
|
||||
if not kwargs:
|
||||
kwargs = {}
|
||||
|
||||
total_vulns = max(0, total_vulns_found - total_ignored)
|
||||
|
||||
vuln_text = 'vulnerabilities' if total_ignored > 1 else 'vulnerability'
|
||||
pkg_text = 'packages were' if len(ignored.keys()) > 1 else 'package was'
|
||||
|
||||
policy_file_text = ' using a safety policy file' if is_using_a_safety_policy_file() else ''
|
||||
|
||||
vuln_brief = f" {total_vulns} vulnerabilit{'y was' if total_vulns == 1 else 'ies were'} found."
|
||||
ignored_text = f' {total_ignored} {vuln_text} from {len(ignored.keys())} {pkg_text} ignored.' if ignored else ''
|
||||
remediation_text = f" {total_remediations} remediation{' was' if total_remediations == 1 else 's were'} " \
|
||||
f"recommended." if is_using_api_key() else ''
|
||||
|
||||
raw_brief = f"Scan was completed{policy_file_text}.{vuln_brief}{ignored_text}{remediation_text}"
|
||||
|
||||
return format_long_text(raw_brief, start_line_decorator=' ', **kwargs)
|
||||
|
||||
|
||||
def get_final_brief_license(licenses, kwargs=None):
|
||||
if not kwargs:
|
||||
kwargs = {}
|
||||
|
||||
licenses_text = ' Scan was completed.'
|
||||
|
||||
if licenses:
|
||||
licenses_text = 'The following software licenses were present in your system: {0}'.format(', '.join(licenses))
|
||||
|
||||
return format_long_text("{0}".format(licenses_text), start_line_decorator=' ', **kwargs)
|
||||
|
||||
|
||||
def format_long_text(text, color='', columns=get_terminal_size().columns, start_line_decorator=' ', end_line_decorator=' ', left_padding='', max_lines=None, styling=None, indent='', sub_indent=''):
|
||||
if not styling:
|
||||
styling = {}
|
||||
|
||||
if color:
|
||||
styling.update({'fg': color})
|
||||
|
||||
columns -= len(start_line_decorator) + len(end_line_decorator)
|
||||
formatted_lines = []
|
||||
lines = text.replace('\r', '').splitlines()
|
||||
|
||||
for line in lines:
|
||||
base_format = "{:" + str(columns) + "}"
|
||||
if line == '':
|
||||
empty_line = base_format.format(" ")
|
||||
formatted_lines.append("{0}{1}{2}".format(start_line_decorator, empty_line, end_line_decorator))
|
||||
wrapped_lines = textwrap.wrap(line, width=columns, max_lines=max_lines, initial_indent=indent, subsequent_indent=sub_indent, placeholder='...')
|
||||
for wrapped_line in wrapped_lines:
|
||||
try:
|
||||
new_line = left_padding + wrapped_line.encode('utf-8')
|
||||
except TypeError:
|
||||
new_line = left_padding + wrapped_line
|
||||
|
||||
if styling:
|
||||
new_line = click.style(new_line, **styling)
|
||||
|
||||
formatted_lines.append(f"{start_line_decorator}{new_line}{end_line_decorator}")
|
||||
|
||||
return "\n".join(formatted_lines)
|
||||
|
||||
|
||||
def get_printable_list_of_scanned_items(scanning_target):
|
||||
context = SafetyContext()
|
||||
|
||||
result = []
|
||||
scanned_items_data = []
|
||||
|
||||
if scanning_target == 'environment':
|
||||
locations = set([pkg.found for pkg in context.packages if isinstance(pkg, Package)])
|
||||
|
||||
for path in locations:
|
||||
result.append([{'styled': False, 'value': '-> ' + path}])
|
||||
scanned_items_data.append(path)
|
||||
|
||||
if len(locations) <= 0:
|
||||
msg = 'No locations found in the environment'
|
||||
result.append([{'styled': False, 'value': msg}])
|
||||
scanned_items_data.append(msg)
|
||||
|
||||
elif scanning_target == 'stdin':
|
||||
scanned_stdin = [pkg.name for pkg in context.packages if isinstance(pkg, Package)]
|
||||
value = 'No found packages in stdin'
|
||||
scanned_items_data = [value]
|
||||
|
||||
if len(scanned_stdin) > 0:
|
||||
value = ', '.join(scanned_stdin)
|
||||
scanned_items_data = scanned_stdin
|
||||
|
||||
result.append(
|
||||
[{'styled': False, 'value': value}])
|
||||
|
||||
elif scanning_target == 'files':
|
||||
for file in context.params.get('files', []):
|
||||
result.append([{'styled': False, 'value': f'-> {file.name}'}])
|
||||
scanned_items_data.append(file.name)
|
||||
elif scanning_target == 'file':
|
||||
file = context.params.get('file', None)
|
||||
name = file.name if file else ''
|
||||
result.append([{'styled': False, 'value': f'-> {name}'}])
|
||||
scanned_items_data.append(name)
|
||||
|
||||
return result, scanned_items_data
|
||||
|
||||
|
||||
REPORT_HEADING = format_long_text(click.style('REPORT', bold=True))
|
||||
|
||||
|
||||
def build_report_brief_section(columns=None, primary_announcement=None, report_type=1, **kwargs):
|
||||
if not columns:
|
||||
columns = get_terminal_size().columns
|
||||
|
||||
styled_brief_lines = []
|
||||
|
||||
if primary_announcement:
|
||||
styled_brief_lines.append(
|
||||
build_primary_announcement(columns=columns, primary_announcement=primary_announcement))
|
||||
|
||||
for line in get_report_brief_info(report_type=report_type, **kwargs):
|
||||
ln = ''
|
||||
padding = ' ' * 2
|
||||
|
||||
for i, words in enumerate(line):
|
||||
processed_words = words.get('value', '')
|
||||
if words.get('style', False):
|
||||
text = ''
|
||||
if i == 0:
|
||||
text = padding
|
||||
padding = ''
|
||||
text += processed_words
|
||||
|
||||
processed_words = click.style(text, bold=True)
|
||||
|
||||
ln += processed_words
|
||||
|
||||
styled_brief_lines.append(format_long_text(ln, color='', columns=columns, start_line_decorator='',
|
||||
left_padding=padding, end_line_decorator='', sub_indent=' ' * 2))
|
||||
|
||||
return "\n".join([add_empty_line(), REPORT_HEADING, add_empty_line(), '\n'.join(styled_brief_lines)])
|
||||
|
||||
|
||||
def build_report_for_review_vuln_report(as_dict=False):
|
||||
ctx = SafetyContext()
|
||||
report_from_file = ctx.review
|
||||
packages = ctx.packages
|
||||
|
||||
if as_dict:
|
||||
return report_from_file
|
||||
|
||||
policy_f_name = report_from_file.get('policy_file', None)
|
||||
safety_policy_used = []
|
||||
if policy_f_name:
|
||||
safety_policy_used = [
|
||||
{'style': False, 'value': '\nScanning using a security policy file'},
|
||||
{'style': True, 'value': ' {0}'.format(policy_f_name)},
|
||||
]
|
||||
|
||||
action_executed = [
|
||||
{'style': True, 'value': 'Scanning dependencies'},
|
||||
{'style': False, 'value': ' in your '},
|
||||
{'style': True, 'value': report_from_file.get('scan_target', '-') + ':'},
|
||||
]
|
||||
|
||||
scanned_items = []
|
||||
|
||||
for name in report_from_file.get('scanned', []):
|
||||
scanned_items.append([{'styled': False, 'value': '-> ' + name}])
|
||||
|
||||
nl = [{'style': False, 'value': ''}]
|
||||
using_sentence = build_using_sentence(report_from_file.get('api_key', None),
|
||||
report_from_file.get('local_database_path_used', None))
|
||||
scanned_count_sentence = build_scanned_count_sentence(packages)
|
||||
old_timestamp = report_from_file.get('timestamp', None)
|
||||
|
||||
old_timestamp = [{'style': False, 'value': 'Report generated '}, {'style': True, 'value': old_timestamp}]
|
||||
now = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||||
current_timestamp = [{'style': False, 'value': 'Timestamp '}, {'style': True, 'value': now}]
|
||||
|
||||
brief_info = [[{'style': False, 'value': 'Safety '},
|
||||
{'style': True, 'value': 'v' + report_from_file.get('safety_version', '-')},
|
||||
{'style': False, 'value': ' is scanning for '},
|
||||
{'style': True, 'value': 'Vulnerabilities'},
|
||||
{'style': True, 'value': '...'}] + safety_policy_used, action_executed
|
||||
] + [nl] + scanned_items + [nl] + [using_sentence] + [scanned_count_sentence] + [old_timestamp] + \
|
||||
[current_timestamp]
|
||||
|
||||
return brief_info
|
||||
|
||||
|
||||
def build_using_sentence(key, db):
|
||||
key_sentence = []
|
||||
custom_integration = os.environ.get('SAFETY_CUSTOM_INTEGRATION',
|
||||
'false').lower() == 'true'
|
||||
|
||||
if key:
|
||||
key_sentence = [{'style': True, 'value': 'an API KEY'},
|
||||
{'style': False, 'value': ' and the '}]
|
||||
db_name = 'PyUp Commercial'
|
||||
elif db:
|
||||
if is_a_remote_mirror(db):
|
||||
if custom_integration:
|
||||
return []
|
||||
db_name = f"remote URL {db}"
|
||||
else:
|
||||
db_name = f"local file {db}"
|
||||
else:
|
||||
db_name = 'non-commercial'
|
||||
|
||||
database_sentence = [{'style': True, 'value': db_name + ' database'}]
|
||||
|
||||
return [{'style': False, 'value': 'Using '}] + key_sentence + database_sentence
|
||||
|
||||
|
||||
def build_scanned_count_sentence(packages):
|
||||
scanned_count = 'No packages found'
|
||||
if len(packages) >= 1:
|
||||
scanned_count = 'Found and scanned {0} {1}'.format(len(packages),
|
||||
'packages' if len(packages) > 1 else 'package')
|
||||
|
||||
return [{'style': True, 'value': scanned_count}]
|
||||
|
||||
|
||||
def add_warnings_if_needed(brief_info):
|
||||
ctx = SafetyContext()
|
||||
warnings = []
|
||||
|
||||
if ctx.packages:
|
||||
if ctx.params.get('continue_on_error', False):
|
||||
warnings += [[{'style': True,
|
||||
'value': '* Continue-on-error is enabled, so returning successful (0) exit code in all cases.'}]]
|
||||
|
||||
if ctx.params.get('ignore_severity_rules', False) and not is_using_api_key():
|
||||
warnings += [[{'style': True,
|
||||
'value': '* Could not filter by severity, please upgrade your account to include severity data.'}]]
|
||||
|
||||
if warnings:
|
||||
brief_info += [[{'style': False, 'value': ''}]] + warnings
|
||||
|
||||
|
||||
def get_report_brief_info(as_dict=False, report_type=1, **kwargs):
|
||||
LOG.info('get_report_brief_info: %s, %s, %s', as_dict, report_type, kwargs)
|
||||
|
||||
context = SafetyContext()
|
||||
|
||||
packages = [pkg for pkg in context.packages if isinstance(pkg, Package)]
|
||||
brief_data = {}
|
||||
command = context.command
|
||||
|
||||
if command == 'review':
|
||||
review = build_report_for_review_vuln_report(as_dict)
|
||||
return review
|
||||
|
||||
key = context.key
|
||||
db = context.db_mirror
|
||||
|
||||
scanning_types = {'check': {'name': 'Vulnerabilities', 'action': 'Scanning dependencies', 'scanning_target': 'environment'}, # Files, Env or Stdin
|
||||
'license': {'name': 'Licenses', 'action': 'Scanning licenses', 'scanning_target': 'environment'}, # Files or Env
|
||||
'review': {'name': 'Report', 'action': 'Reading the report',
|
||||
'scanning_target': 'file'}} # From file
|
||||
|
||||
targets = ['stdin', 'environment', 'files', 'file']
|
||||
for target in targets:
|
||||
if context.params.get(target, False):
|
||||
scanning_types[command]['scanning_target'] = target
|
||||
break
|
||||
|
||||
scanning_target = scanning_types.get(context.command, {}).get('scanning_target', '')
|
||||
brief_data['scan_target'] = scanning_target
|
||||
scanned_items, data = get_printable_list_of_scanned_items(scanning_target)
|
||||
brief_data['scanned'] = data
|
||||
nl = [{'style': False, 'value': ''}]
|
||||
|
||||
action_executed = [
|
||||
{'style': True, 'value': scanning_types.get(context.command, {}).get('action', '')},
|
||||
{'style': False, 'value': ' in your '},
|
||||
{'style': True, 'value': scanning_target + ':'},
|
||||
]
|
||||
|
||||
policy_file = context.params.get('policy_file', None)
|
||||
safety_policy_used = []
|
||||
|
||||
brief_data['policy_file'] = policy_file.get('filename', '-') if policy_file else None
|
||||
brief_data['policy_file_source'] = 'server' if brief_data['policy_file'] and 'server-safety-policy' in brief_data['policy_file'] else 'local'
|
||||
|
||||
if policy_file and policy_file.get('filename', False):
|
||||
safety_policy_used = [
|
||||
{'style': False, 'value': '\nScanning using a security policy file'},
|
||||
{'style': True, 'value': ' {0}'.format(policy_file.get('filename', '-'))},
|
||||
]
|
||||
|
||||
audit_and_monitor = []
|
||||
if context.params.get('audit_and_monitor'):
|
||||
logged_url = context.params.get('audit_and_monitor_url') if context.params.get('audit_and_monitor_url') else "https://pyup.io"
|
||||
audit_and_monitor = [
|
||||
{'style': False, 'value': '\nLogging scan results to'},
|
||||
{'style': True, 'value': ' {0}'.format(logged_url)},
|
||||
]
|
||||
|
||||
current_time = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||||
|
||||
brief_data['api_key'] = bool(key)
|
||||
brief_data['local_database_path'] = db if db else None
|
||||
brief_data['safety_version'] = get_safety_version()
|
||||
brief_data['timestamp'] = current_time
|
||||
brief_data['packages_found'] = len(packages)
|
||||
# Vuln report
|
||||
additional_data = []
|
||||
if report_type == 1:
|
||||
brief_data['vulnerabilities_found'] = kwargs.get('vulnerabilities_found', 0)
|
||||
brief_data['vulnerabilities_ignored'] = kwargs.get('vulnerabilities_ignored', 0)
|
||||
brief_data['remediations_recommended'] = 0
|
||||
|
||||
additional_data = [
|
||||
[{'style': True, 'value': str(brief_data['vulnerabilities_found'])},
|
||||
{'style': True, 'value': f' vulnerabilit{"y" if brief_data["vulnerabilities_found"] == 1 else "ies"} found'}],
|
||||
[{'style': True, 'value': str(brief_data['vulnerabilities_ignored'])},
|
||||
{'style': True, 'value': f' vulnerabilit{"y" if brief_data["vulnerabilities_ignored"] == 1 else "ies"} ignored'}],
|
||||
]
|
||||
|
||||
if is_using_api_key():
|
||||
brief_data['remediations_recommended'] = kwargs.get('remediations_recommended', 0)
|
||||
additional_data.extend(
|
||||
[[{'style': True, 'value': str(brief_data['remediations_recommended'])},
|
||||
{'style': True, 'value':
|
||||
f' remediation{"" if brief_data["remediations_recommended"] == 1 else "s"} recommended'}]])
|
||||
|
||||
elif report_type == 2:
|
||||
brief_data['licenses_found'] = kwargs.get('licenses_found', 0)
|
||||
additional_data = [
|
||||
[{'style': True, 'value': str(brief_data['licenses_found'])},
|
||||
{'style': True, 'value': f' license {"type" if brief_data["licenses_found"] == 1 else "types"} found'}],
|
||||
]
|
||||
|
||||
brief_data['telemetry'] = build_telemetry_data()
|
||||
|
||||
brief_data['git'] = build_git_data()
|
||||
brief_data['project'] = context.params.get('project', None)
|
||||
|
||||
brief_data['json_version'] = 1
|
||||
|
||||
using_sentence = build_using_sentence(key, db)
|
||||
using_sentence_section = [nl] if not using_sentence else [nl] + [build_using_sentence(key, db)]
|
||||
scanned_count_sentence = build_scanned_count_sentence(packages)
|
||||
|
||||
timestamp = [{'style': False, 'value': 'Timestamp '}, {'style': True, 'value': current_time}]
|
||||
|
||||
brief_info = [[{'style': False, 'value': 'Safety '},
|
||||
{'style': True, 'value': 'v' + get_safety_version()},
|
||||
{'style': False, 'value': ' is scanning for '},
|
||||
{'style': True, 'value': scanning_types.get(context.command, {}).get('name', '')},
|
||||
{'style': True, 'value': '...'}] + safety_policy_used + audit_and_monitor, action_executed
|
||||
] + [nl] + scanned_items + using_sentence_section + [scanned_count_sentence] + [timestamp]
|
||||
|
||||
brief_info.extend(additional_data)
|
||||
|
||||
add_warnings_if_needed(brief_info)
|
||||
|
||||
LOG.info('Brief info data: %s', brief_data)
|
||||
LOG.info('Brief info, styled output: %s', '\n\n LINE ---->\n ' + '\n\n LINE ---->\n '.join(map(str, brief_info)))
|
||||
|
||||
return brief_data if as_dict else brief_info
|
||||
|
||||
|
||||
def build_primary_announcement(primary_announcement, columns=None, only_text=False):
|
||||
lines = json.loads(primary_announcement.get('message'))
|
||||
|
||||
for line in lines:
|
||||
if 'words' not in line:
|
||||
raise ValueError('Missing words keyword')
|
||||
if len(line['words']) <= 0:
|
||||
raise ValueError('No words in this line')
|
||||
for word in line['words']:
|
||||
if 'value' not in word or not word['value']:
|
||||
raise ValueError('Empty word or without value')
|
||||
|
||||
message = style_lines(lines, columns, start_line='', end_line='')
|
||||
|
||||
return click.unstyle(message) if only_text else message
|
||||
|
||||
|
||||
def is_using_api_key():
|
||||
return bool(SafetyContext().key)
|
||||
|
||||
|
||||
def is_using_a_safety_policy_file():
|
||||
return bool(SafetyContext().params.get('policy_file', None))
|
||||
|
||||
|
||||
def should_add_nl(output, found_vulns):
|
||||
if output == 'bare' and not found_vulns:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Safety Security and License Configuration file
|
||||
# We recommend checking this file into your source control in the root of your Python project
|
||||
# If this file is named .safety-policy.yml and is in the same directory where you run `safety check` it will be used by default.
|
||||
# Otherwise, you can use the flag `safety check --policy-file <path-to-this-file>` to specify a custom location and name for the file.
|
||||
# To validate and review your policy file, run the validate command: `safety validate policy_file --path <path-to-this-file>`
|
||||
security: # configuration for the `safety check` command
|
||||
ignore-cvss-severity-below: 0 # A severity number between 0 and 10. Some helpful reference points: 9=ignore all vulnerabilities except CRITICAL severity. 7=ignore all vulnerabilities except CRITICAL & HIGH severity. 4=ignore all vulnerabilities except CRITICAL, HIGH & MEDIUM severity.
|
||||
ignore-cvss-unknown-severity: False # True or False. We recommend you set this to False.
|
||||
ignore-vulnerabilities: # Here you can list multiple specific vulnerabilities you want to ignore (optionally for a time period)
|
||||
# We recommend making use of the optional `reason` and `expires` keys for each vulnerability that you ignore.
|
||||
25853: # Example vulnerability ID
|
||||
reason: we don't use the vulnerable function # optional, for internal note purposes to communicate with your team. This reason will be reported in the Safety reports
|
||||
expires: '2022-10-21' # datetime string - date this ignore will expire, best practice to use this variable
|
||||
continue-on-vulnerability-error: False # Suppress non-zero exit codes when vulnerabilities are found. Enable this in pipelines and CI/CD processes if you want to pass builds that have vulnerabilities. We recommend you set this to False.
|
||||
alert: # configuration for the `safety alert` command
|
||||
security:
|
||||
# Configuration specific to Safety's GitHub Issue alerting
|
||||
github-issue:
|
||||
# Same as for security - these allow controlling if this alert will fire based
|
||||
# on severity information.
|
||||
# default: not set
|
||||
# ignore-cvss-severity-below: 6
|
||||
# ignore-cvss-unknown-severity: False
|
||||
|
||||
# Add a label to pull requests with the cvss severity, if available
|
||||
# default: true
|
||||
# label-severity: True
|
||||
|
||||
# Add a label to pull requests, default is 'security'
|
||||
# requires private repo permissions, even on public repos
|
||||
# default: security
|
||||
# labels:
|
||||
# - security
|
||||
|
||||
# Assign users to pull requests, default is not set
|
||||
# requires private repo permissions, even on public repos
|
||||
# default: empty
|
||||
# assignees:
|
||||
# - example-user
|
||||
|
||||
# Prefix to give issues when creating them. Note that changing
|
||||
# this might cause duplicate issues to be created.
|
||||
# default: "[PyUp] "
|
||||
# issue-prefix: "[PyUp] "
|
||||
|
||||
# Configuration specific to Safety's GitHub PR alerting
|
||||
github-pr:
|
||||
# Same as for security - these allow controlling if this alert will fire based
|
||||
# on severity information.
|
||||
# default: not set
|
||||
# ignore-cvss-severity-below: 6
|
||||
# ignore-cvss-unknown-severity: False
|
||||
|
||||
# Set the default branch (ie, main, master)
|
||||
# default: empty, the default branch on GitHub
|
||||
branch: ''
|
||||
|
||||
# Add a label to pull requests with the cvss severity, if available
|
||||
# default: true
|
||||
# label-severity: True
|
||||
|
||||
# Add a label to pull requests, default is 'security'
|
||||
# requires private repo permissions, even on public repos
|
||||
# default: security
|
||||
# labels:
|
||||
# - security
|
||||
|
||||
# Assign users to pull requests, default is not set
|
||||
# requires private repo permissions, even on public repos
|
||||
# default: empty
|
||||
# assignees:
|
||||
# - example-user
|
||||
|
||||
# Configure the branch prefix for PRs created by this alert.
|
||||
# NB: Changing this will likely cause duplicate PRs.
|
||||
# default: pyup/
|
||||
branch-prefix: pyup/
|
||||
|
||||
# Set a global prefix for PRs
|
||||
# default: "[PyUp] "
|
||||
pr-prefix: "[PyUp] "
|
||||
+478
-78
@@ -1,42 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import errno
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
|
||||
import pipenv.patched.pip._vendor.requests as requests
|
||||
from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
|
||||
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version, Version, LegacyVersion, parse
|
||||
|
||||
from .constants import (API_MIRRORS, CACHE_FILE, CACHE_LICENSES_VALID_SECONDS,
|
||||
CACHE_VALID_SECONDS, OPEN_MIRRORS, REQUEST_TIMEOUT)
|
||||
from .constants import (API_MIRRORS, CACHE_FILE, OPEN_MIRRORS, REQUEST_TIMEOUT, API_BASE_URL)
|
||||
from .errors import (DatabaseFetchError, DatabaseFileNotFoundError,
|
||||
InvalidKeyError, TooManyRequestsError)
|
||||
from .util import RequirementFile
|
||||
InvalidKeyError, TooManyRequestsError, NetworkConnectionError,
|
||||
RequestTimeoutError, ServerError, MalformedDatabase)
|
||||
from .models import Vulnerability, CVE, Severity
|
||||
from .util import RequirementFile, read_requirements, Package, build_telemetry_data, sync_safety_context, SafetyContext, \
|
||||
validate_expiration_date, is_a_remote_mirror
|
||||
|
||||
session = requests.session()
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Vulnerability(namedtuple("Vulnerability",
|
||||
["name", "spec", "version", "advisory", "vuln_id", "cvssv2", "cvssv3"])):
|
||||
pass
|
||||
|
||||
|
||||
def get_from_cache(db_name):
|
||||
def get_from_cache(db_name, cache_valid_seconds=0):
|
||||
LOG.debug('Trying to get from cache...')
|
||||
if os.path.exists(CACHE_FILE):
|
||||
LOG.info('Cache file path: %s', CACHE_FILE)
|
||||
with open(CACHE_FILE) as f:
|
||||
try:
|
||||
data = json.loads(f.read())
|
||||
LOG.debug('Trying to get the %s from the cache file', db_name)
|
||||
LOG.debug('Databases in CACHE file: %s', ', '.join(data))
|
||||
if db_name in data:
|
||||
if "cached_at" in data[db_name]:
|
||||
if 'licenses.json' in db_name:
|
||||
# Getting the specific cache time for the licenses db.
|
||||
cache_valid_seconds = CACHE_LICENSES_VALID_SECONDS
|
||||
else:
|
||||
cache_valid_seconds = CACHE_VALID_SECONDS
|
||||
LOG.debug('db_name %s', db_name)
|
||||
|
||||
if "cached_at" in data[db_name]:
|
||||
if data[db_name]["cached_at"] + cache_valid_seconds > time.time():
|
||||
LOG.debug('Getting the database from cache at %s, cache setting: %s',
|
||||
data[db_name]["cached_at"], cache_valid_seconds)
|
||||
return data[db_name]["db"]
|
||||
|
||||
LOG.debug('Cached file is too old, it was cached at %s', data[db_name]["cached_at"])
|
||||
else:
|
||||
LOG.debug('There is not the cached_at key in %s database', data[db_name])
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
LOG.debug('JSONDecodeError trying to get the cached database.')
|
||||
else:
|
||||
LOG.debug("Cache file doesn't exist...")
|
||||
return False
|
||||
|
||||
|
||||
@@ -58,7 +73,9 @@ def write_to_cache(db_name, data):
|
||||
os.makedirs(os.path.dirname(CACHE_FILE))
|
||||
with open(CACHE_FILE, "w") as _:
|
||||
_.write(json.dumps({}))
|
||||
LOG.debug('Cache file created')
|
||||
except OSError as exc: # Guard against race condition
|
||||
LOG.debug('Unable to create the cache file because: %s', exc.errno)
|
||||
if exc.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
@@ -66,6 +83,7 @@ def write_to_cache(db_name, data):
|
||||
try:
|
||||
cache = json.loads(f.read())
|
||||
except json.JSONDecodeError:
|
||||
LOG.debug('JSONDecodeError in the local cache, dumping the full cache file.')
|
||||
cache = {}
|
||||
|
||||
with open(CACHE_FILE, "w") as f:
|
||||
@@ -74,51 +92,134 @@ def write_to_cache(db_name, data):
|
||||
"db": data
|
||||
}
|
||||
f.write(json.dumps(cache))
|
||||
LOG.debug('Safety updated the cache file for %s database.', db_name)
|
||||
|
||||
|
||||
def fetch_database_url(mirror, db_name, key, cached, proxy):
|
||||
|
||||
def fetch_database_url(mirror, db_name, key, cached, proxy, telemetry=True):
|
||||
headers = {}
|
||||
if key:
|
||||
headers["X-Api-Key"] = key
|
||||
|
||||
if not proxy:
|
||||
proxy = {}
|
||||
|
||||
if cached:
|
||||
cached_data = get_from_cache(db_name=db_name)
|
||||
cached_data = get_from_cache(db_name=db_name, cache_valid_seconds=cached)
|
||||
if cached_data:
|
||||
LOG.info('Database %s returned from cache.', db_name)
|
||||
return cached_data
|
||||
url = mirror + db_name
|
||||
r = requests.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy)
|
||||
if r.status_code == 200:
|
||||
|
||||
telemetry_data = {'telemetry': json.dumps(build_telemetry_data(telemetry=telemetry))}
|
||||
|
||||
try:
|
||||
r = session.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy, params=telemetry_data)
|
||||
except requests.exceptions.ConnectionError:
|
||||
raise NetworkConnectionError()
|
||||
except requests.exceptions.Timeout:
|
||||
raise RequestTimeoutError()
|
||||
except requests.exceptions.RequestException:
|
||||
raise DatabaseFetchError()
|
||||
|
||||
if r.status_code == 403:
|
||||
raise InvalidKeyError(key=key, reason=r.text)
|
||||
|
||||
if r.status_code == 429:
|
||||
raise TooManyRequestsError(reason=r.text)
|
||||
|
||||
if r.status_code != 200:
|
||||
raise ServerError(reason=r.reason)
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
if cached:
|
||||
write_to_cache(db_name, data)
|
||||
return data
|
||||
elif r.status_code == 403:
|
||||
raise InvalidKeyError()
|
||||
elif r.status_code == 429:
|
||||
raise TooManyRequestsError()
|
||||
except json.JSONDecodeError as e:
|
||||
raise MalformedDatabase(reason=e)
|
||||
|
||||
if cached:
|
||||
LOG.info('Writing %s to cache because cached value was %s', db_name, cached)
|
||||
write_to_cache(db_name, data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def fetch_policy(key, proxy):
|
||||
url = f"{API_BASE_URL}policy/"
|
||||
headers = {"X-Api-Key": key}
|
||||
|
||||
if not proxy:
|
||||
proxy = {}
|
||||
|
||||
try:
|
||||
LOG.debug(f'Getting policy')
|
||||
r = session.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy)
|
||||
LOG.debug(r.text)
|
||||
return r.json()
|
||||
except:
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
LOG.exception("Error fetching policy")
|
||||
click.secho(
|
||||
"Warning: couldn't fetch policy from pyup.io.",
|
||||
fg="yellow",
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
return {"safety_policy": "", "audit_and_monitor": False}
|
||||
|
||||
|
||||
def post_results(key, proxy, safety_json, policy_file):
|
||||
url = f"{API_BASE_URL}result/"
|
||||
headers = {"X-Api-Key": key}
|
||||
|
||||
if not proxy:
|
||||
proxy = {}
|
||||
|
||||
# safety_json is in text form already. policy_file is a text YAML
|
||||
audit_report = {
|
||||
"safety_json": json.loads(safety_json),
|
||||
"policy_file": policy_file
|
||||
}
|
||||
|
||||
try:
|
||||
LOG.debug(f'Posting results: {audit_report}')
|
||||
r = session.post(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy, json=audit_report)
|
||||
LOG.debug(r.text)
|
||||
|
||||
return r.json()
|
||||
except:
|
||||
import pipenv.vendor.click as click
|
||||
|
||||
LOG.exception("Error posting results")
|
||||
click.secho(
|
||||
"Warning: couldn't upload results to pyup.io.",
|
||||
fg="yellow",
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_database_file(path, db_name):
|
||||
full_path = os.path.join(path, db_name)
|
||||
if not os.path.exists(full_path):
|
||||
raise DatabaseFileNotFoundError()
|
||||
raise DatabaseFileNotFoundError(db=path)
|
||||
with open(full_path) as f:
|
||||
return json.loads(f.read())
|
||||
|
||||
|
||||
def fetch_database(full=False, key=False, db=False, cached=False, proxy={}):
|
||||
|
||||
if db:
|
||||
def fetch_database(full=False, key=False, db=False, cached=0, proxy=None, telemetry=True):
|
||||
if key:
|
||||
mirrors = API_MIRRORS
|
||||
elif db:
|
||||
mirrors = [db]
|
||||
else:
|
||||
mirrors = API_MIRRORS if key else OPEN_MIRRORS
|
||||
mirrors = OPEN_MIRRORS
|
||||
|
||||
db_name = "insecure_full.json" if full else "insecure.json"
|
||||
for mirror in mirrors:
|
||||
# mirror can either be a local path or a URL
|
||||
if mirror.startswith("http://") or mirror.startswith("https://"):
|
||||
data = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy)
|
||||
if is_a_remote_mirror(mirror):
|
||||
data = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy, telemetry=telemetry)
|
||||
else:
|
||||
data = fetch_database_file(mirror, db_name=db_name)
|
||||
if data:
|
||||
@@ -133,12 +234,97 @@ def get_vulnerabilities(pkg, spec, db):
|
||||
yield entry
|
||||
|
||||
|
||||
def check(packages, key, db_mirror, cached, ignore_ids, proxy):
|
||||
key = key if key else os.environ.get("SAFETY_API_KEY", False)
|
||||
db = fetch_database(key=key, db=db_mirror, cached=cached, proxy=proxy)
|
||||
def get_vulnerability_from(vuln_id, cve, data, specifier, db, name, pkg, ignore_vulns):
|
||||
base_domain = db.get('$meta', {}).get('base_domain')
|
||||
pkg_meta = db.get('$meta', {}).get('packages', {}).get(name, {})
|
||||
insecure_versions = pkg_meta.get("insecure_versions", [])
|
||||
secure_versions = pkg_meta.get("secure_versions", [])
|
||||
latest_version_without_known_vulnerabilities = pkg_meta.get("latest_secure_version", None)
|
||||
latest_version = pkg_meta.get("latest_version", None)
|
||||
pkg_refreshed = pkg._replace(insecure_versions=insecure_versions, secure_versions=secure_versions,
|
||||
latest_version_without_known_vulnerabilities=latest_version_without_known_vulnerabilities,
|
||||
latest_version=latest_version,
|
||||
more_info_url=f"{base_domain}{pkg_meta.get('more_info_path', '')}")
|
||||
|
||||
ignored = (ignore_vulns and vuln_id in ignore_vulns and (
|
||||
not ignore_vulns[vuln_id]['expires'] or ignore_vulns[vuln_id]['expires'] > datetime.utcnow()))
|
||||
more_info_url = f"{base_domain}{data.get('more_info_path', '')}"
|
||||
severity = None
|
||||
|
||||
if cve and (cve.cvssv2 or cve.cvssv3):
|
||||
severity = Severity(source=cve.name, cvssv2=cve.cvssv2, cvssv3=cve.cvssv3)
|
||||
|
||||
return Vulnerability(
|
||||
vulnerability_id=vuln_id,
|
||||
package_name=name,
|
||||
pkg=pkg_refreshed,
|
||||
ignored=ignored,
|
||||
ignored_reason=ignore_vulns.get(vuln_id, {}).get('reason', None) if ignore_vulns else None,
|
||||
ignored_expires=ignore_vulns.get(vuln_id, {}).get('expires', None) if ignore_vulns else None,
|
||||
vulnerable_spec=specifier,
|
||||
all_vulnerable_specs=data.get("specs", []),
|
||||
analyzed_version=pkg_refreshed.version,
|
||||
advisory=data.get("advisory"),
|
||||
is_transitive=data.get("transitive", False),
|
||||
published_date=data.get("published_date"),
|
||||
fixed_versions=[ver for ver in data.get("fixed_versions", []) if ver],
|
||||
closest_versions_without_known_vulnerabilities=data.get("closest_secure_versions", []),
|
||||
resources=data.get("vulnerability_resources"),
|
||||
CVE=cve,
|
||||
severity=severity,
|
||||
affected_versions=data.get("affected_versions", []),
|
||||
more_info_url=more_info_url
|
||||
)
|
||||
|
||||
|
||||
def get_cve_from(data, db_full):
|
||||
cve_data = data.get("cve", '')
|
||||
|
||||
if not cve_data:
|
||||
return None
|
||||
|
||||
cve_id = cve_data.split(",")[0].strip()
|
||||
cve_meta = db_full.get("$meta", {}).get("cve", {}).get(cve_id, {})
|
||||
return CVE(name=cve_id, cvssv2=cve_meta.get("cvssv2", None),
|
||||
cvssv3=cve_meta.get("cvssv3", None))
|
||||
|
||||
|
||||
def ignore_vuln_if_needed(vuln_id, cve, ignore_vulns, ignore_severity_rules):
|
||||
|
||||
if not ignore_severity_rules or not isinstance(ignore_vulns, dict):
|
||||
return
|
||||
|
||||
severity = None
|
||||
|
||||
if cve:
|
||||
if cve.cvssv2 and cve.cvssv2.get("base_score", None):
|
||||
severity = cve.cvssv2.get("base_score", None)
|
||||
|
||||
if cve.cvssv3 and cve.cvssv3.get("base_score", None):
|
||||
severity = cve.cvssv3.get("base_score", None)
|
||||
|
||||
ignore_severity_below = float(ignore_severity_rules.get('ignore-cvss-severity-below', 0.0))
|
||||
ignore_unknown_severity = bool(ignore_severity_rules.get('ignore-cvss-unknown-severity', False))
|
||||
|
||||
if severity:
|
||||
if float(severity) < ignore_severity_below:
|
||||
reason = 'Ignored by severity rule in policy file, {0} < {1}'.format(float(severity),
|
||||
ignore_severity_below)
|
||||
ignore_vulns[vuln_id] = {'reason': reason, 'expires': None}
|
||||
elif ignore_unknown_severity:
|
||||
reason = 'Unknown CVSS severity, ignored by severity rule in policy file.'
|
||||
ignore_vulns[vuln_id] = {'reason': reason, 'expires': None}
|
||||
|
||||
|
||||
@sync_safety_context
|
||||
def check(packages, key=False, db_mirror=False, cached=0, ignore_vulns=None, ignore_severity_rules=None, proxy=None,
|
||||
include_ignored=False, is_env_scan=True, telemetry=True, params=None, project=None):
|
||||
SafetyContext().command = 'check'
|
||||
db = fetch_database(key=key, db=db_mirror, cached=cached, proxy=proxy, telemetry=telemetry)
|
||||
db_full = None
|
||||
vulnerable_packages = frozenset(db.keys())
|
||||
vulnerable = []
|
||||
vulnerabilities = []
|
||||
|
||||
for pkg in packages:
|
||||
# Ignore recursive files not resolved
|
||||
if isinstance(pkg, RequirementFile):
|
||||
@@ -146,7 +332,7 @@ def check(packages, key, db_mirror, cached, ignore_ids, proxy):
|
||||
|
||||
# normalize the package name, the safety-db is converting underscores to dashes and uses
|
||||
# lowercase
|
||||
name = pkg.key.replace("_", "-").lower()
|
||||
name = canonicalize_name(pkg.name)
|
||||
|
||||
if name in vulnerable_packages:
|
||||
# we have a candidate here, build the spec set
|
||||
@@ -154,51 +340,175 @@ def check(packages, key, db_mirror, cached, ignore_ids, proxy):
|
||||
spec_set = SpecifierSet(specifiers=specifier)
|
||||
if spec_set.contains(pkg.version):
|
||||
if not db_full:
|
||||
db_full = fetch_database(full=True, key=key, db=db_mirror, cached=cached, proxy=proxy)
|
||||
db_full = fetch_database(full=True, key=key, db=db_mirror, cached=cached, proxy=proxy,
|
||||
telemetry=telemetry)
|
||||
for data in get_vulnerabilities(pkg=name, spec=specifier, db=db_full):
|
||||
vuln_id = data.get("id").replace("pyup.io-", "")
|
||||
cve_id = data.get("cve")
|
||||
if cve_id:
|
||||
cve_id = cve_id.split(",")[0].strip()
|
||||
if vuln_id and vuln_id not in ignore_ids:
|
||||
cve_meta = db_full.get("$meta", {}).get("cve", {}).get(cve_id, {})
|
||||
vulnerable.append(
|
||||
Vulnerability(
|
||||
name=name,
|
||||
spec=specifier,
|
||||
version=pkg.version,
|
||||
advisory=data.get("advisory"),
|
||||
vuln_id=vuln_id,
|
||||
cvssv2=cve_meta.get("cvssv2", None),
|
||||
cvssv3=cve_meta.get("cvssv3", None),
|
||||
)
|
||||
)
|
||||
return vulnerable
|
||||
cve = get_cve_from(data, db_full)
|
||||
|
||||
ignore_vuln_if_needed(vuln_id, cve, ignore_vulns, ignore_severity_rules)
|
||||
|
||||
vulnerability = get_vulnerability_from(vuln_id, cve, data, specifier, db_full, name, pkg,
|
||||
ignore_vulns)
|
||||
|
||||
should_add_vuln = not (vulnerability.is_transitive and is_env_scan)
|
||||
|
||||
if (include_ignored or vulnerability.vulnerability_id not in ignore_vulns) and should_add_vuln:
|
||||
vulnerabilities.append(vulnerability)
|
||||
|
||||
return vulnerabilities, db_full
|
||||
|
||||
|
||||
def review(vulnerabilities):
|
||||
def precompute_remediations(remediations, package_metadata, vulns,
|
||||
ignored_vulns):
|
||||
for vuln in vulns:
|
||||
if vuln.ignored:
|
||||
ignored_vulns.add(vuln.vulnerability_id)
|
||||
continue
|
||||
|
||||
if vuln.package_name in remediations.keys():
|
||||
remediations[vuln.package_name]['vulns_found'] = remediations[vuln.package_name].get('vulns_found', 0) + 1
|
||||
else:
|
||||
vulns_count = 1
|
||||
package_metadata[vuln.package_name] = {'insecure_versions': vuln.pkg.insecure_versions,
|
||||
'secure_versions': vuln.pkg.secure_versions, 'version': vuln.pkg.version}
|
||||
remediations[vuln.package_name] = {'vulns_found': vulns_count, 'version': vuln.pkg.version,
|
||||
'more_info_url': vuln.pkg.more_info_url}
|
||||
|
||||
|
||||
def get_closest_ver(versions, version):
|
||||
results = {'minor': None, 'major': None}
|
||||
if not version or not versions:
|
||||
return results
|
||||
|
||||
sorted_versions = sorted(versions, key=lambda ver: parse_version(ver), reverse=True)
|
||||
|
||||
for v in sorted_versions:
|
||||
index = parse_version(v)
|
||||
current_v = parse_version(version)
|
||||
|
||||
if index > current_v:
|
||||
results['major'] = index
|
||||
|
||||
if index < current_v:
|
||||
results['minor'] = index
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def compute_sec_ver_for_user(package, ignored_vulns, db_full):
|
||||
pkg_meta = db_full.get('$meta', {}).get('packages', {}).get(package, {})
|
||||
versions = set(pkg_meta.get("insecure_versions", []) + pkg_meta.get("secure_versions", []))
|
||||
affected_versions = []
|
||||
|
||||
for vuln in db_full.get(package, []):
|
||||
vuln_id = vuln.get('id', None)
|
||||
if vuln_id and vuln_id not in ignored_vulns:
|
||||
affected_versions += vuln.get('affected_versions', [])
|
||||
|
||||
affected_v = set(affected_versions)
|
||||
sec_ver_for_user = list(versions.difference(affected_v))
|
||||
|
||||
return sorted(sec_ver_for_user, key=lambda ver: parse_version(ver), reverse=True)
|
||||
|
||||
|
||||
def compute_sec_ver(remediations, package_metadata, ignored_vulns, db_full):
|
||||
"""
|
||||
Compute the secure_versions and the closest_secure_version for each remediation using the affected_versions
|
||||
of each no ignored vulnerability of the same package, there is only a remediation for each package.
|
||||
"""
|
||||
for pkg_name in remediations.keys():
|
||||
pkg = package_metadata.get(pkg_name, {})
|
||||
|
||||
if not ignored_vulns:
|
||||
secure_v = pkg.get('secure_versions', [])
|
||||
else:
|
||||
secure_v = compute_sec_ver_for_user(package=pkg_name, ignored_vulns=ignored_vulns, db_full=db_full)
|
||||
|
||||
remediations[pkg_name]['secure_versions'] = secure_v
|
||||
remediations[pkg_name]['closest_secure_version'] = get_closest_ver(secure_v,
|
||||
pkg.get('version', None))
|
||||
|
||||
|
||||
def calculate_remediations(vulns, db_full):
|
||||
remediations = {}
|
||||
package_metadata = {}
|
||||
ignored_vulns = set()
|
||||
|
||||
if not db_full:
|
||||
return remediations
|
||||
|
||||
precompute_remediations(remediations, package_metadata, vulns, ignored_vulns)
|
||||
compute_sec_ver(remediations, package_metadata, ignored_vulns, db_full)
|
||||
|
||||
return remediations
|
||||
|
||||
|
||||
@sync_safety_context
|
||||
def review(report=None, params=None):
|
||||
SafetyContext().command = 'review'
|
||||
vulnerable = []
|
||||
vulnerabilities = report.get('vulnerabilities', []) + report.get('ignored_vulnerabilities', [])
|
||||
remediations = {}
|
||||
|
||||
for key, value in report.get('remediations', {}).items():
|
||||
recommended = value.get('recommended_version', None)
|
||||
secure_v = value.get('other_recommended_versions', [])
|
||||
major = None
|
||||
if recommended:
|
||||
secure_v.append(recommended)
|
||||
major = parse(recommended)
|
||||
|
||||
remediations[key] = {'vulns_found': value.get('vulnerabilities_found', 0),
|
||||
'version': value.get('current_version'),
|
||||
'secure_versions': secure_v,
|
||||
'closest_secure_version': {'major': major, 'minor': None},
|
||||
# minor isn't supported in review
|
||||
'more_info_url': value.get('more_info_url')}
|
||||
|
||||
packages = report.get('scanned_packages', [])
|
||||
pkgs = {pkg_name: Package(**pkg_values) for pkg_name, pkg_values in packages.items()}
|
||||
ctx = SafetyContext()
|
||||
found_packages = list(pkgs.values())
|
||||
ctx.packages = found_packages
|
||||
ctx.review = report.get('report_meta', [])
|
||||
ctx.key = ctx.review.get('api_key', False)
|
||||
cvssv2 = None
|
||||
cvssv3 = None
|
||||
|
||||
for vuln in vulnerabilities:
|
||||
current_vuln = {
|
||||
"name": vuln[0],
|
||||
"spec": vuln[1],
|
||||
"version": vuln[2],
|
||||
"advisory": vuln[3],
|
||||
"vuln_id": vuln[4],
|
||||
"cvssv2": None,
|
||||
"cvssv3": None
|
||||
}
|
||||
vulnerable.append(
|
||||
Vulnerability(**current_vuln)
|
||||
)
|
||||
return vulnerable
|
||||
vuln['pkg'] = pkgs.get(vuln.get('package_name', None))
|
||||
XVE_ID = vuln.get('CVE', None) # Trying to get first the CVE ID
|
||||
|
||||
severity = vuln.get('severity', None)
|
||||
if severity and severity.get('source', False):
|
||||
cvssv2 = severity.get('cvssv2', None)
|
||||
cvssv3 = severity.get('cvssv3', None)
|
||||
# Trying to get the PVE ID if it exists, otherwise it will be the same CVE ID of above
|
||||
XVE_ID = severity.get('source', False)
|
||||
vuln['severity'] = Severity(source=XVE_ID, cvssv2=cvssv2, cvssv3=cvssv3)
|
||||
else:
|
||||
vuln['severity'] = None
|
||||
|
||||
ignored_expires = vuln.get('ignored_expires', None)
|
||||
|
||||
if ignored_expires:
|
||||
vuln['ignored_expires'] = validate_expiration_date(ignored_expires)
|
||||
|
||||
vuln['CVE'] = CVE(name=XVE_ID, cvssv2=cvssv2, cvssv3=cvssv3) if XVE_ID else None
|
||||
|
||||
vulnerable.append(Vulnerability(**vuln))
|
||||
|
||||
return vulnerable, remediations, found_packages
|
||||
|
||||
|
||||
def get_licenses(key, db_mirror, cached, proxy):
|
||||
@sync_safety_context
|
||||
def get_licenses(key=False, db_mirror=False, cached=0, proxy=None, telemetry=True):
|
||||
key = key if key else os.environ.get("SAFETY_API_KEY", False)
|
||||
|
||||
if not key and not db_mirror:
|
||||
raise InvalidKeyError("The API-KEY was not provided.")
|
||||
raise InvalidKeyError(message="The API-KEY was not provided.")
|
||||
if db_mirror:
|
||||
mirrors = [db_mirror]
|
||||
else:
|
||||
@@ -208,10 +518,100 @@ def get_licenses(key, db_mirror, cached, proxy):
|
||||
|
||||
for mirror in mirrors:
|
||||
# mirror can either be a local path or a URL
|
||||
if mirror.startswith("http://") or mirror.startswith("https://"):
|
||||
licenses = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy)
|
||||
if is_a_remote_mirror(mirror):
|
||||
licenses = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy,
|
||||
telemetry=telemetry)
|
||||
else:
|
||||
licenses = fetch_database_file(mirror, db_name=db_name)
|
||||
if licenses:
|
||||
return licenses
|
||||
raise DatabaseFetchError()
|
||||
|
||||
|
||||
def get_announcements(key, proxy, telemetry=True):
|
||||
LOG.info('Getting announcements')
|
||||
|
||||
announcements = []
|
||||
headers = {}
|
||||
|
||||
if key:
|
||||
headers["X-Api-Key"] = key
|
||||
|
||||
url = f"{API_BASE_URL}announcements/"
|
||||
method = 'post'
|
||||
data = build_telemetry_data(telemetry=telemetry)
|
||||
request_kwargs = {'headers': headers, 'proxies': proxy, 'timeout': 3}
|
||||
data_keyword = 'json'
|
||||
|
||||
source = os.environ.get('SAFETY_ANNOUNCEMENTS_URL', None)
|
||||
|
||||
if source:
|
||||
LOG.debug(f'Getting the announcement from a different source: {source}')
|
||||
url = source
|
||||
method = 'get'
|
||||
data = {
|
||||
'telemetry': json.dumps(data)}
|
||||
data_keyword = 'params'
|
||||
|
||||
request_kwargs[data_keyword] = data
|
||||
request_kwargs['url'] = url
|
||||
|
||||
LOG.debug(f'Telemetry data sent: {data}')
|
||||
|
||||
try:
|
||||
request_func = getattr(session, method)
|
||||
r = request_func(**request_kwargs)
|
||||
LOG.debug(r.text)
|
||||
except Exception as e:
|
||||
LOG.info('Unexpected but HANDLED Exception happened getting the announcements: %s', e)
|
||||
return announcements
|
||||
|
||||
if r.status_code == 200:
|
||||
try:
|
||||
announcements = r.json()
|
||||
if 'announcements' in announcements.keys():
|
||||
announcements = announcements.get('announcements', [])
|
||||
else:
|
||||
LOG.info('There is not announcements key in the JSON response, is this a wrong structure?')
|
||||
announcements = []
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
LOG.info('Unexpected but HANDLED Exception happened decoding the announcement response: %s', e)
|
||||
|
||||
LOG.info('Announcements fetched')
|
||||
|
||||
return announcements
|
||||
|
||||
|
||||
def get_packages(files=False, stdin=False):
|
||||
|
||||
if files:
|
||||
return list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
|
||||
|
||||
if stdin:
|
||||
return list(read_requirements(sys.stdin))
|
||||
|
||||
import pipenv.patched.pip._vendor.pkg_resources as pkg_resources
|
||||
|
||||
return [
|
||||
Package(name=d.key, version=d.version, found=d.location, insecure_versions=[], secure_versions=[],
|
||||
latest_version=None, latest_version_without_known_vulnerabilities=None, more_info_url=None) for d in
|
||||
pkg_resources.working_set
|
||||
if d.key not in {"python", "wsgiref", "argparse"}
|
||||
]
|
||||
|
||||
|
||||
def read_vulnerabilities(fh):
|
||||
try:
|
||||
data = json.load(fh)
|
||||
except json.JSONDecodeError as e:
|
||||
raise MalformedDatabase(reason=e, fetched_from=fh.name)
|
||||
except TypeError as e:
|
||||
raise MalformedDatabase(reason=e, fetched_from=fh.name)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def close_session():
|
||||
LOG.debug('Closing requests session.')
|
||||
session.close()
|
||||
|
||||
+601
-103
@@ -1,33 +1,39 @@
|
||||
from pipenv.vendor.dparse.parser import setuptools_parse_requirements_backport as _parse_requirements
|
||||
from collections import namedtuple
|
||||
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
|
||||
import pipenv.vendor.click as click
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
Package = namedtuple("Package", ["key", "version"])
|
||||
RequirementFile = namedtuple("RequirementFile", ["path"])
|
||||
import platform
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from difflib import SequenceMatcher
|
||||
from threading import Lock
|
||||
from typing import List
|
||||
|
||||
import pipenv.vendor.click as click
|
||||
from pipenv.vendor.click import BadParameter
|
||||
from pipenv.vendor.dparse import parse, filetypes
|
||||
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
|
||||
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
|
||||
from pipenv.vendor.ruamel.yaml import YAML
|
||||
from pipenv.vendor.ruamel.yaml.error import MarkedYAMLError
|
||||
|
||||
from pipenv.patched.safety.constants import EXIT_CODE_FAILURE, EXIT_CODE_OK
|
||||
from pipenv.patched.safety.models import Package, RequirementFile
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read_vulnerabilities(fh):
|
||||
return json.load(fh)
|
||||
def is_a_remote_mirror(mirror):
|
||||
return mirror.startswith("http://") or mirror.startswith("https://")
|
||||
|
||||
|
||||
def iter_lines(fh, lineno=0):
|
||||
for line in fh.readlines()[lineno:]:
|
||||
yield line
|
||||
def is_supported_by_parser(path):
|
||||
supported_types = (".txt", ".in", ".yml", ".ini", "Pipfile",
|
||||
"Pipfile.lock", "setup.cfg", "poetry.lock")
|
||||
return path.endswith(supported_types)
|
||||
|
||||
|
||||
def parse_line(line):
|
||||
if line.startswith('-e') or line.startswith('http://') or line.startswith('https://'):
|
||||
if "#egg=" in line:
|
||||
line = line.split("#egg=")[-1]
|
||||
if ' --hash' in line:
|
||||
line = line.split(" --hash")[0]
|
||||
return _parse_requirements(line)
|
||||
|
||||
|
||||
def read_requirements(fh, resolve=False):
|
||||
def read_requirements(fh, resolve=True):
|
||||
"""
|
||||
Reads requirements from a file like object and (optionally) from referenced files.
|
||||
:param fh: file like object to read from
|
||||
@@ -35,88 +41,52 @@ def read_requirements(fh, resolve=False):
|
||||
:return: generator
|
||||
"""
|
||||
is_temp_file = not hasattr(fh, 'name')
|
||||
for num, line in enumerate(iter_lines(fh)):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
# skip empty lines
|
||||
continue
|
||||
if line.startswith('#') or \
|
||||
line.startswith('-i') or \
|
||||
line.startswith('--index-url') or \
|
||||
line.startswith('--extra-index-url') or \
|
||||
line.startswith('-f') or line.startswith('--find-links') or \
|
||||
line.startswith('--no-index') or line.startswith('--allow-external') or \
|
||||
line.startswith('--allow-unverified') or line.startswith('-Z') or \
|
||||
line.startswith('--always-unzip'):
|
||||
# skip unsupported lines
|
||||
continue
|
||||
elif line.startswith('-r') or line.startswith('--requirement'):
|
||||
# got a referenced file here, try to resolve the path
|
||||
# if this is a tempfile, skip
|
||||
if is_temp_file:
|
||||
continue
|
||||
path = None
|
||||
found = 'temp_file'
|
||||
file_type = filetypes.requirements_txt
|
||||
|
||||
# strip away the recursive flag
|
||||
prefixes = ["-r", "--requirement"]
|
||||
filename = line.strip()
|
||||
for prefix in prefixes:
|
||||
if filename.startswith(prefix):
|
||||
filename = filename[len(prefix):].strip()
|
||||
if not is_temp_file and is_supported_by_parser(fh.name):
|
||||
LOG.debug('not temp and a compatible file')
|
||||
path = fh.name
|
||||
found = path
|
||||
file_type = None
|
||||
|
||||
# if there is a comment, remove it
|
||||
if " #" in filename:
|
||||
filename = filename.split(" #")[0].strip()
|
||||
req_file_path = os.path.join(os.path.dirname(fh.name), filename)
|
||||
if resolve:
|
||||
# recursively yield the resolved requirements
|
||||
if os.path.exists(req_file_path):
|
||||
with open(req_file_path) as _fh:
|
||||
for req in read_requirements(_fh, resolve=True):
|
||||
yield req
|
||||
else:
|
||||
yield RequirementFile(path=req_file_path)
|
||||
else:
|
||||
try:
|
||||
parseable_line = line
|
||||
# multiline requirements are not parseable
|
||||
if "\\" in line:
|
||||
parseable_line = line.replace("\\", "")
|
||||
for next_line in iter_lines(fh, num + 1):
|
||||
parseable_line += next_line.strip().replace("\\", "")
|
||||
line += "\n" + next_line
|
||||
if "\\" in next_line:
|
||||
continue
|
||||
break
|
||||
req, = parse_line(parseable_line)
|
||||
if len(req.specifier._specs) == 1 and \
|
||||
next(iter(req.specifier._specs))._spec[0] == "==":
|
||||
yield Package(key=req.name, version=next(iter(req.specifier._specs))._spec[1])
|
||||
else:
|
||||
try:
|
||||
fname = fh.name
|
||||
except AttributeError:
|
||||
fname = line
|
||||
LOG.debug(f'Path: {path}')
|
||||
LOG.debug(f'File Type: {file_type}')
|
||||
LOG.debug('Trying to parse file using dparse...')
|
||||
content = fh.read()
|
||||
LOG.debug(f'Content: {content}')
|
||||
dependency_file = parse(content, path=path, resolve=resolve,
|
||||
file_type=file_type)
|
||||
LOG.debug(f'Dependency file: {dependency_file.serialize()}')
|
||||
LOG.debug(f'Parsed, dependencies: {[dep.serialize() for dep in dependency_file.resolved_dependencies]}')
|
||||
for dep in dependency_file.resolved_dependencies:
|
||||
try:
|
||||
spec = next(iter(dep.specs))._spec
|
||||
except StopIteration:
|
||||
click.secho(
|
||||
f"Warning: unpinned requirement '{dep.name}' found in {path}, "
|
||||
"unable to check.",
|
||||
fg="yellow",
|
||||
file=sys.stderr
|
||||
)
|
||||
return
|
||||
|
||||
click.secho(
|
||||
"Warning: unpinned requirement '{req}' found in {fname}, "
|
||||
"unable to check.".format(req=req.name,
|
||||
fname=fname),
|
||||
fg="yellow",
|
||||
file=sys.stderr
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
version = spec[1]
|
||||
if spec[0] == '==':
|
||||
yield Package(name=dep.name, version=version,
|
||||
found=found,
|
||||
insecure_versions=[],
|
||||
secure_versions=[], latest_version=None,
|
||||
latest_version_without_known_vulnerabilities=None,
|
||||
more_info_url=None)
|
||||
|
||||
|
||||
def get_proxy_dict(proxyprotocol, proxyhost, proxyport):
|
||||
proxy_dictionary = {}
|
||||
if proxyhost is not None:
|
||||
if proxyprotocol in ["http", "https"]:
|
||||
proxy_dictionary = {proxyprotocol: "{0}://{1}:{2}".format(proxyprotocol, proxyhost, str(proxyport))}
|
||||
else:
|
||||
click.secho("Proxy Protocol should be http or https only.", fg="red")
|
||||
sys.exit(-1)
|
||||
return proxy_dictionary
|
||||
def get_proxy_dict(proxy_protocol, proxy_host, proxy_port):
|
||||
if proxy_protocol and proxy_host and proxy_port:
|
||||
# Safety only uses https request, so only https dict will be passed to requests
|
||||
return {'https': f"{proxy_protocol}://{proxy_host}:{str(proxy_port)}"}
|
||||
return None
|
||||
|
||||
|
||||
def get_license_name_by_id(license_id, db):
|
||||
@@ -126,13 +96,541 @@ def get_license_name_by_id(license_id, db):
|
||||
return name
|
||||
return None
|
||||
|
||||
def get_packages_licenses(packages, licenses_db):
|
||||
"""Get the licenses for the specified packages based on their version.
|
||||
|
||||
def get_flags_from_context():
|
||||
flags = {}
|
||||
context = click.get_current_context(silent=True)
|
||||
|
||||
if context:
|
||||
for option in context.command.params:
|
||||
flags_per_opt = option.opts + option.secondary_opts
|
||||
for flag in flags_per_opt:
|
||||
flags[flag] = option.name
|
||||
|
||||
return flags
|
||||
|
||||
|
||||
def get_used_options():
|
||||
flags = get_flags_from_context()
|
||||
used_options = {}
|
||||
|
||||
for arg in sys.argv:
|
||||
cleaned_arg = arg if '=' not in arg else arg.split('=')[0]
|
||||
if cleaned_arg in flags:
|
||||
option_used = flags.get(cleaned_arg)
|
||||
|
||||
if option_used in used_options:
|
||||
used_options[option_used][cleaned_arg] = used_options[option_used].get(cleaned_arg, 0) + 1
|
||||
else:
|
||||
used_options[option_used] = {cleaned_arg: 1}
|
||||
|
||||
return used_options
|
||||
|
||||
|
||||
def get_safety_version():
|
||||
from pipenv.patched.safety import VERSION
|
||||
return VERSION
|
||||
|
||||
|
||||
def get_primary_announcement(announcements):
|
||||
for announcement in announcements:
|
||||
if announcement.get('type', '').lower() == 'primary_announcement':
|
||||
try:
|
||||
from pipenv.patched.safety.output_utils import build_primary_announcement
|
||||
build_primary_announcement(announcement, columns=80)
|
||||
except Exception as e:
|
||||
LOG.debug(f'Failed to build primary announcement: {str(e)}')
|
||||
return None
|
||||
|
||||
return announcement
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_basic_announcements(announcements):
|
||||
return [announcement for announcement in announcements if
|
||||
announcement.get('type', '').lower() != 'primary_announcement']
|
||||
|
||||
|
||||
def filter_announcements(announcements, by_type='error'):
|
||||
return [announcement for announcement in announcements if
|
||||
announcement.get('type', '').lower() == by_type]
|
||||
|
||||
|
||||
def build_telemetry_data(telemetry=True):
|
||||
context = SafetyContext()
|
||||
|
||||
body = {
|
||||
'os_type': os.environ.get("SAFETY_OS_TYPE", None) or platform.system(),
|
||||
'os_release': os.environ.get("SAFETY_OS_RELEASE", None) or platform.release(),
|
||||
'os_description': os.environ.get("SAFETY_OS_DESCRIPTION", None) or platform.platform(),
|
||||
'python_version': platform.python_version(),
|
||||
'safety_command': context.command,
|
||||
'safety_options': get_used_options()
|
||||
} if telemetry else {}
|
||||
|
||||
body['safety_version'] = get_safety_version()
|
||||
body['safety_source'] = os.environ.get("SAFETY_SOURCE", None) or context.safety_source
|
||||
|
||||
LOG.debug(f'Telemetry body built: {body}')
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def build_git_data():
|
||||
import subprocess
|
||||
|
||||
def git_command(commandline):
|
||||
return subprocess.run(commandline, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('utf-8').strip()
|
||||
|
||||
try:
|
||||
is_git = git_command(["git", "rev-parse", "--is-inside-work-tree"])
|
||||
except Exception:
|
||||
is_git = False
|
||||
|
||||
if is_git == "true":
|
||||
result = {
|
||||
"branch": "",
|
||||
"tag": "",
|
||||
"commit": "",
|
||||
"dirty": "",
|
||||
"origin": ""
|
||||
}
|
||||
|
||||
try:
|
||||
result['branch'] = git_command(["git", "symbolic-ref", "--short", "-q", "HEAD"])
|
||||
result['tag'] = git_command(["git", "describe", "--tags", "--exact-match"])
|
||||
|
||||
commit = git_command(["git", "describe", '--match=""', '--always', '--abbrev=40', '--dirty'])
|
||||
result['dirty'] = commit.endswith('-dirty')
|
||||
result['commit'] = commit.split("-dirty")[0]
|
||||
|
||||
result['origin'] = git_command(["git", "remote", "get-url", "origin"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
else:
|
||||
return {
|
||||
"error": "not-git-repo"
|
||||
}
|
||||
|
||||
|
||||
def output_exception(exception, exit_code_output=True):
|
||||
click.secho(str(exception), fg="red", file=sys.stderr)
|
||||
|
||||
if exit_code_output:
|
||||
exit_code = EXIT_CODE_FAILURE
|
||||
if hasattr(exception, 'get_exit_code'):
|
||||
exit_code = exception.get_exit_code()
|
||||
else:
|
||||
exit_code = EXIT_CODE_OK
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def get_processed_options(policy_file, ignore, ignore_severity_rules, exit_code):
|
||||
if policy_file:
|
||||
security = policy_file.get('security', {})
|
||||
source = click.get_current_context().get_parameter_source("exit_code")
|
||||
|
||||
if not ignore:
|
||||
ignore = security.get('ignore-vulnerabilities', {})
|
||||
if source == click.core.ParameterSource.DEFAULT:
|
||||
exit_code = not security.get('continue-on-vulnerability-error', False)
|
||||
ignore_cvss_below = security.get('ignore-cvss-severity-below', 0.0)
|
||||
ignore_cvss_unknown = security.get('ignore-cvss-unknown-severity', False)
|
||||
ignore_severity_rules = {'ignore-cvss-severity-below': ignore_cvss_below,
|
||||
'ignore-cvss-unknown-severity': ignore_cvss_unknown}
|
||||
|
||||
return ignore, ignore_severity_rules, exit_code
|
||||
|
||||
|
||||
class MutuallyExclusiveOption(click.Option):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.mutually_exclusive = set(kwargs.pop('mutually_exclusive', []))
|
||||
self.with_values = kwargs.pop('with_values', {})
|
||||
help = kwargs.get('help', '')
|
||||
if self.mutually_exclusive:
|
||||
ex_str = ', '.join(["{0} with values {1}".format(item, self.with_values.get(item)) if item in self.with_values else item for item in self.mutually_exclusive])
|
||||
kwargs['help'] = help + (
|
||||
' NOTE: This argument is mutually exclusive with '
|
||||
' arguments: [' + ex_str + '].'
|
||||
)
|
||||
super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)
|
||||
|
||||
def handle_parse_result(self, ctx, opts, args):
|
||||
m_exclusive_used = self.mutually_exclusive.intersection(opts)
|
||||
option_used = m_exclusive_used and self.name in opts
|
||||
|
||||
exclusive_value_used = False
|
||||
for used in m_exclusive_used:
|
||||
value_used = opts.get(used, None)
|
||||
if not isinstance(value_used, List):
|
||||
value_used = [value_used]
|
||||
if value_used and set(self.with_values.get(used, [])).intersection(value_used):
|
||||
exclusive_value_used = True
|
||||
|
||||
if option_used and (not self.with_values or exclusive_value_used):
|
||||
options = ', '.join(self.opts)
|
||||
prohibited = ''.join(["\n * --{0} with {1}".format(item, self.with_values.get(
|
||||
item)) if item in self.with_values else f"\n * {item}" for item in self.mutually_exclusive])
|
||||
raise click.UsageError(
|
||||
f"Illegal usage: `{options}` is mutually exclusive with: {prohibited}"
|
||||
)
|
||||
|
||||
return super(MutuallyExclusiveOption, self).handle_parse_result(
|
||||
ctx,
|
||||
opts,
|
||||
args
|
||||
)
|
||||
|
||||
|
||||
class DependentOption(click.Option):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.required_options = set(kwargs.pop('required_options', []))
|
||||
help = kwargs.get('help', '')
|
||||
if self.required_options:
|
||||
ex_str = ', '.join(self.required_options)
|
||||
kwargs['help'] = help + (
|
||||
' NOTE: This argument requires the following flags '
|
||||
' [' + ex_str + '].'
|
||||
)
|
||||
super(DependentOption, self).__init__(*args, **kwargs)
|
||||
|
||||
def handle_parse_result(self, ctx, opts, args):
|
||||
missing_required_arguments = self.required_options.difference(opts) and self.name in opts
|
||||
|
||||
if missing_required_arguments:
|
||||
raise click.UsageError(
|
||||
"Illegal usage: `{}` needs the "
|
||||
"arguments `{}`.".format(
|
||||
self.name,
|
||||
', '.join(missing_required_arguments)
|
||||
)
|
||||
)
|
||||
|
||||
return super(DependentOption, self).handle_parse_result(
|
||||
ctx,
|
||||
opts,
|
||||
args
|
||||
)
|
||||
|
||||
|
||||
def transform_ignore(ctx, param, value):
|
||||
if isinstance(value, tuple):
|
||||
return dict(zip(value, [{'reason': '', 'expires': None} for _ in range(len(value))]))
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def active_color_if_needed(ctx, param, value):
|
||||
if value == 'screen':
|
||||
ctx.color = True
|
||||
|
||||
color = os.environ.get("SAFETY_COLOR", None)
|
||||
|
||||
if color is not None:
|
||||
color = color.lower()
|
||||
|
||||
if color == '1' or color == 'true':
|
||||
ctx.color = True
|
||||
elif color == '0' or color == 'false':
|
||||
ctx.color = False
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def json_alias(ctx, param, value):
|
||||
if value:
|
||||
os.environ['SAFETY_OUTPUT'] = 'json'
|
||||
return value
|
||||
|
||||
|
||||
def bare_alias(ctx, param, value):
|
||||
if value:
|
||||
os.environ['SAFETY_OUTPUT'] = 'bare'
|
||||
return value
|
||||
|
||||
|
||||
def get_terminal_size():
|
||||
from shutil import get_terminal_size as t_size
|
||||
# get_terminal_size can report 0, 0 if run from pseudo-terminal prior Python 3.11 versions
|
||||
|
||||
columns = t_size().columns or 80
|
||||
lines = t_size().lines or 24
|
||||
|
||||
return os.terminal_size((columns, lines))
|
||||
|
||||
|
||||
def validate_expiration_date(expiration_date):
|
||||
d = None
|
||||
|
||||
if expiration_date:
|
||||
try:
|
||||
d = datetime.strptime(expiration_date, '%Y-%m-%d')
|
||||
except ValueError as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
d = datetime.strptime(expiration_date, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError as e:
|
||||
pass
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class SafetyPolicyFile(click.ParamType):
|
||||
"""
|
||||
Custom Safety Policy file to hold validations
|
||||
"""
|
||||
|
||||
name = "filename"
|
||||
envvar_list_splitter = os.path.pathsep
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mode: str = "r",
|
||||
encoding: str = None,
|
||||
errors: str = "strict",
|
||||
pure: bool = os.environ.get('SAFETY_PURE_YAML', 'false').lower() == 'true'
|
||||
) -> None:
|
||||
self.mode = mode
|
||||
self.encoding = encoding
|
||||
self.errors = errors
|
||||
self.basic_msg = '\n' + click.style('Unable to load the Safety Policy file "{name}".', fg='red')
|
||||
self.pure = pure
|
||||
|
||||
def to_info_dict(self):
|
||||
info_dict = super().to_info_dict()
|
||||
info_dict.update(mode=self.mode, encoding=self.encoding)
|
||||
return info_dict
|
||||
|
||||
def fail_if_unrecognized_keys(self, used_keys, valid_keys, param=None, ctx=None, msg='{hint}', context_hint=''):
|
||||
for keyword in used_keys:
|
||||
if keyword not in valid_keys:
|
||||
match = None
|
||||
max_ratio = 0.0
|
||||
if isinstance(keyword, str):
|
||||
for option in valid_keys:
|
||||
ratio = SequenceMatcher(None, keyword, option).ratio()
|
||||
if ratio > max_ratio:
|
||||
match = option
|
||||
max_ratio = ratio
|
||||
|
||||
maybe_msg = f' Maybe you meant: {match}' if max_ratio > 0.7 else \
|
||||
f' Valid keywords in this level are: {", ".join(valid_keys)}'
|
||||
|
||||
self.fail(msg.format(hint=f'{context_hint}"{keyword}" is not a valid keyword.{maybe_msg}'), param, ctx)
|
||||
|
||||
def fail_if_wrong_bool_value(self, keyword, value, msg='{hint}'):
|
||||
if value is not None and not isinstance(value, bool):
|
||||
self.fail(msg.format(hint=f"'{keyword}' value needs to be a boolean. "
|
||||
"You can use True, False, TRUE, FALSE, true or false"))
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
|
||||
if hasattr(value, "read") or hasattr(value, "write"):
|
||||
return value
|
||||
|
||||
msg = self.basic_msg.format(name=value) + '\n' + click.style('HINT:', fg='yellow') + ' {hint}'
|
||||
|
||||
f, _ = click.types.open_stream(
|
||||
value, self.mode, self.encoding, self.errors, atomic=False
|
||||
)
|
||||
filename = ''
|
||||
|
||||
try:
|
||||
raw = f.read()
|
||||
yaml = YAML(typ='safe', pure=self.pure)
|
||||
safety_policy = yaml.load(raw)
|
||||
filename = f.name
|
||||
f.close()
|
||||
except Exception as e:
|
||||
show_parsed_hint = isinstance(e, MarkedYAMLError)
|
||||
hint = str(e)
|
||||
if show_parsed_hint:
|
||||
hint = f'{str(e.problem).strip()} {str(e.context).strip()} {str(e.context_mark).strip()}'
|
||||
|
||||
self.fail(msg.format(name=value, hint=hint), param, ctx)
|
||||
|
||||
if not safety_policy or not isinstance(safety_policy, dict) or not safety_policy.get('security', None):
|
||||
self.fail(
|
||||
msg.format(hint='you are missing the security root tag'), param, ctx)
|
||||
|
||||
security_config = safety_policy.get('security', {})
|
||||
security_keys = ['ignore-cvss-severity-below', 'ignore-cvss-unknown-severity', 'ignore-vulnerabilities',
|
||||
'continue-on-vulnerability-error']
|
||||
used_keys = security_config.keys()
|
||||
|
||||
self.fail_if_unrecognized_keys(used_keys, security_keys, param=param, ctx=ctx, msg=msg,
|
||||
context_hint='"security" -> ')
|
||||
|
||||
ignore_cvss_security_below = security_config.get('ignore-cvss-severity-below', None)
|
||||
|
||||
if ignore_cvss_security_below:
|
||||
limit = 0.0
|
||||
|
||||
try:
|
||||
limit = float(ignore_cvss_security_below)
|
||||
except ValueError as e:
|
||||
self.fail(msg.format(hint="'ignore-cvss-severity-below' value needs to be an integer or float."))
|
||||
|
||||
if limit < 0 or limit > 10:
|
||||
self.fail(msg.format(hint="'ignore-cvss-severity-below' needs to be a value between 0 and 10"))
|
||||
|
||||
continue_on_vulnerability_error = security_config.get('continue-on-vulnerability-error', None)
|
||||
self.fail_if_wrong_bool_value('continue-on-vulnerability-error', continue_on_vulnerability_error, msg)
|
||||
|
||||
ignore_cvss_unknown_severity = security_config.get('ignore-cvss-unknown-severity', None)
|
||||
self.fail_if_wrong_bool_value('ignore-cvss-unknown-severity', ignore_cvss_unknown_severity, msg)
|
||||
|
||||
ignore_vulns = safety_policy.get('security', {}).get('ignore-vulnerabilities', {})
|
||||
|
||||
if ignore_vulns:
|
||||
if not isinstance(ignore_vulns, dict):
|
||||
self.fail(msg.format(hint="Vulnerability IDs under the 'ignore-vulnerabilities' key, need to "
|
||||
"follow the convention 'ID_NUMBER:', probably you are missing a colon."))
|
||||
|
||||
normalized = {}
|
||||
|
||||
for ignored_vuln_id, config in ignore_vulns.items():
|
||||
ignored_vuln_config = config if config else {}
|
||||
|
||||
if not isinstance(ignored_vuln_config, dict):
|
||||
self.fail(
|
||||
msg.format(hint=f"Wrong configuration under the vulnerability with ID: {ignored_vuln_id}"))
|
||||
|
||||
context_msg = f'"security" -> "ignore-vulnerabilities" -> "{ignored_vuln_id}" -> '
|
||||
|
||||
self.fail_if_unrecognized_keys(ignored_vuln_config.keys(), ['reason', 'expires'], param=param,
|
||||
ctx=ctx, msg=msg, context_hint=context_msg)
|
||||
|
||||
reason = ignored_vuln_config.get('reason', '')
|
||||
reason = str(reason) if reason else None
|
||||
expires = ignored_vuln_config.get('expires', '')
|
||||
expires = str(expires) if expires else None
|
||||
|
||||
try:
|
||||
if int(ignored_vuln_id) < 0:
|
||||
raise ValueError('Negative Vulnerability ID')
|
||||
except ValueError as e:
|
||||
self.fail(msg.format(
|
||||
hint=f"vulnerability id {ignored_vuln_id} under the 'ignore-vulnerabilities' root needs to "
|
||||
f"be a positive integer")
|
||||
)
|
||||
|
||||
# Validate expires
|
||||
d = validate_expiration_date(expires)
|
||||
|
||||
if expires and not d:
|
||||
self.fail(msg.format(hint=f"{context_msg}expires: \"{expires}\" isn't a valid format "
|
||||
f"for the expires keyword, "
|
||||
"valid options are: YYYY-MM-DD or "
|
||||
"YYYY-MM-DD HH:MM:SS")
|
||||
)
|
||||
|
||||
normalized[str(ignored_vuln_id)] = {'reason': reason, 'expires': d}
|
||||
|
||||
safety_policy['security']['ignore-vulnerabilities'] = normalized
|
||||
safety_policy['filename'] = filename
|
||||
safety_policy['raw'] = raw
|
||||
else:
|
||||
safety_policy['security']['ignore-vulnerabilities'] = {}
|
||||
|
||||
return safety_policy
|
||||
except BadParameter as expected_e:
|
||||
raise expected_e
|
||||
except Exception as e:
|
||||
# Don't fail in the default case
|
||||
if ctx and isinstance(e, OSError):
|
||||
source = ctx.get_parameter_source("policy_file")
|
||||
if e.errno == 2 and source == click.core.ParameterSource.DEFAULT and value == '.safety-policy.yml':
|
||||
return None
|
||||
|
||||
problem = click.style("Policy file YAML is not valid.")
|
||||
hint = click.style("HINT: ", fg='yellow') + str(e)
|
||||
self.fail(f"{problem}\n{hint}", param, ctx)
|
||||
|
||||
def shell_complete(
|
||||
self, ctx: "Context", param: "Parameter", incomplete: str
|
||||
):
|
||||
"""Return a special completion marker that tells the completion
|
||||
system to use the shell to provide file path completions.
|
||||
|
||||
:param ctx: Invocation context for this command.
|
||||
:param param: The parameter that is requesting completion.
|
||||
:param incomplete: Value being completed. May be empty.
|
||||
|
||||
.. versionadded:: 8.0
|
||||
"""
|
||||
from pipenv.vendor.click.shell_completion import CompletionItem
|
||||
|
||||
return [CompletionItem(incomplete, type="file")]
|
||||
|
||||
|
||||
class SingletonMeta(type):
|
||||
|
||||
_instances = {}
|
||||
|
||||
_lock = Lock()
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
with cls._lock:
|
||||
if cls not in cls._instances:
|
||||
instance = super().__call__(*args, **kwargs)
|
||||
cls._instances[cls] = instance
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class SafetyContext(metaclass=SingletonMeta):
|
||||
packages = None
|
||||
key = False
|
||||
db_mirror = False
|
||||
cached = None
|
||||
ignore_vulns = None
|
||||
ignore_severity_rules = None
|
||||
proxy = None
|
||||
include_ignored = False
|
||||
telemetry = None
|
||||
files = None
|
||||
stdin = None
|
||||
is_env_scan = None
|
||||
command = None
|
||||
review = None
|
||||
params = {}
|
||||
safety_source = 'code'
|
||||
|
||||
|
||||
def sync_safety_context(f):
|
||||
def new_func(*args, **kwargs):
|
||||
ctx = SafetyContext()
|
||||
|
||||
for attr in dir(ctx):
|
||||
if attr in kwargs:
|
||||
setattr(ctx, attr, kwargs.get(attr))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return new_func
|
||||
|
||||
|
||||
@sync_safety_context
|
||||
def get_packages_licenses(packages=None, licenses_db=None):
|
||||
"""Get the licenses for the specified packages based on their version.
|
||||
|
||||
:param packages: packages list
|
||||
:param licenses_db: the licenses db in the raw form.
|
||||
:return: list of objects with the packages and their respectives licenses.
|
||||
"""
|
||||
SafetyContext().command = 'license'
|
||||
|
||||
if not packages:
|
||||
packages = []
|
||||
if not licenses_db:
|
||||
licenses_db = {}
|
||||
|
||||
packages_licenses_db = licenses_db.get('packages', {})
|
||||
filtered_packages_licenses = []
|
||||
|
||||
@@ -141,7 +639,7 @@ def get_packages_licenses(packages, licenses_db):
|
||||
if isinstance(pkg, RequirementFile):
|
||||
continue
|
||||
# normalize the package name
|
||||
pkg_name = pkg.key.replace("_", "-").lower()
|
||||
pkg_name = canonicalize_name(pkg.name)
|
||||
# packages may have different licenses depending their version.
|
||||
pkg_licenses = packages_licenses_db.get(pkg_name, [])
|
||||
version_requested = parse_version(pkg.version)
|
||||
@@ -160,7 +658,7 @@ def get_packages_licenses(packages, licenses_db):
|
||||
if license_id:
|
||||
license_name = get_license_name_by_id(license_id, licenses_db)
|
||||
if not license_id or not license_name:
|
||||
license_name = "N/A"
|
||||
license_name = "unknown"
|
||||
|
||||
filtered_packages_licenses.append({
|
||||
"package": pkg_name,
|
||||
|
||||
Vendored
-237
@@ -1,237 +0,0 @@
|
||||
Release History
|
||||
===============
|
||||
|
||||
2.0.0 / 2021-05-25
|
||||
------------------
|
||||
|
||||
* Drop Python 2.7 and 3.5 support
|
||||
* Make ``termcolor`` an external dependency
|
||||
* Run CI tests under Ubuntu 20.04
|
||||
* Update dependencies
|
||||
|
||||
|
||||
1.5.0 / 2021-03-21
|
||||
------------------
|
||||
|
||||
* Update cli-spinners to ``v2.6.0``
|
||||
* Update dependencies
|
||||
|
||||
|
||||
1.4.1 / 2021-02-28
|
||||
------------------
|
||||
|
||||
* Fix timer round-up behavior (#118)
|
||||
|
||||
|
||||
1.4.0 / 2021-02-21
|
||||
------------------
|
||||
|
||||
* Add spinner timer (#99, #108)
|
||||
* fix(#107): use ``poetry_core`` as build backend
|
||||
* fix(#34): allow ``write()`` to print non-string objects
|
||||
* Update dependencies
|
||||
|
||||
|
||||
1.3.0 / 2021-01-17
|
||||
------------------
|
||||
|
||||
* Optimization: wait of stop event instead of sleep
|
||||
* Update dependencies
|
||||
|
||||
|
||||
1.2.0 / 2020-10-19
|
||||
------------------
|
||||
|
||||
* Update cli-spinners to ``v2.5.0``
|
||||
* Add support for Python 3.9
|
||||
|
||||
|
||||
1.1.0 / 2020-10-04
|
||||
------------------
|
||||
|
||||
* Add ``hidden()`` context manager #68
|
||||
* fix(#70): ``hidden()`` exceptions handling
|
||||
* Replace coveralls.io with codecov.io
|
||||
* Update dependencies
|
||||
|
||||
|
||||
1.0.0 / 2020-08-02
|
||||
------------------
|
||||
|
||||
* "Stabilize" yaspin; ``1.*`` branch will contain stable release with Python 2
|
||||
support. Drop Python 2 and switch to Python 3 completely is planned for versions
|
||||
``2.*``.
|
||||
|
||||
|
||||
0.18.0 / 2020-07-21
|
||||
-------------------
|
||||
|
||||
* Update cli-spinners to ``v2.4.0``
|
||||
* Update dependencies
|
||||
* fix(#59): remove ``tests/`` and ``examples/`` from wheels distribution
|
||||
|
||||
|
||||
0.17.0 / 2020-05-08
|
||||
-------------------
|
||||
|
||||
* Migrate to ``poetry`` for dependencies management, building and publishing project
|
||||
* Add tests for Python 3.8
|
||||
* Deprecate support for Python 3.4
|
||||
* Run tests under Ubuntu 18.04
|
||||
* Update dev dependencies to the most recent ones (compatible with Python 2.7)
|
||||
* Remove Tox from the project (use CI for tests under different versions of Python)
|
||||
|
||||
|
||||
0.16.0 / 2020-01-11
|
||||
-------------------
|
||||
|
||||
* Allow use inside zip bundled package
|
||||
* Code improvements
|
||||
|
||||
|
||||
0.15.0 / 2019-08-09
|
||||
-------------------
|
||||
|
||||
* Update cli-spinners to v2.2.0
|
||||
|
||||
|
||||
0.14.3 / 2019-05-12
|
||||
-------------------
|
||||
|
||||
* fix(#29): race condition between spinner thread and ``write()``
|
||||
|
||||
|
||||
0.14.2 / 2019-04-27
|
||||
-------------------
|
||||
|
||||
* fix: remove extra ``\b`` written to stdout. Fixes ``write()`` in rxvt terminal
|
||||
|
||||
|
||||
0.14.1 / 2019-01-28
|
||||
-------------------
|
||||
|
||||
* fix(#26): traceback on PYTHONOPTIMIZE=2
|
||||
|
||||
|
||||
0.14.0 / 2018-09-05
|
||||
-------------------
|
||||
|
||||
* Support for handling POSIX signals
|
||||
* New function in public API: ``kbi_safe_yaspin``
|
||||
|
||||
|
||||
0.13.0 / 2018-08-14
|
||||
-------------------
|
||||
|
||||
* API improvements: ``spinner``, ``color``, ``on_color``, ``attrs`` and ``side`` argument values are handled via ``__getattr__``
|
||||
* New ``yaspin`` arguments: ``on_color``, ``attrs``
|
||||
* ``right=False`` argument replaced with ``side="left"``
|
||||
* ``Yaspin.right`` replaced with ``Yaspin.side``
|
||||
* ``reverse`` argument replaced with ``reversal``
|
||||
* ``Yaspin.reverse`` replaced with ``Yaspin.reversal``
|
||||
* Remove default text stripping in ``Yaspin._freeze``
|
||||
|
||||
|
||||
0.12.0 / 2018-07-16
|
||||
-------------------
|
||||
|
||||
* Add support for Python 3.7
|
||||
* Drop support for Python 2.6 and 3.3
|
||||
|
||||
* dev: Migrate to Pipfile
|
||||
* dev: Speedup local unittests with pytest-xdist
|
||||
|
||||
|
||||
0.11.1 / 2018-07-10
|
||||
-------------------
|
||||
|
||||
* fix(#16): remove default text stripping in ``Yaspin.write`` to allow printing of the hierarchical text
|
||||
|
||||
|
||||
0.11.0 / 2018-06-23
|
||||
-------------------
|
||||
|
||||
* Update cli-spinners to v1.3.1
|
||||
|
||||
|
||||
0.10.0 / 2018-03-23
|
||||
-------------------
|
||||
|
||||
* New ``hide`` and ``show`` methods to toggle the display of the spinner
|
||||
|
||||
|
||||
0.9.0 / 2018-02-26
|
||||
------------------
|
||||
|
||||
* New ``write`` method for writing text into terminal without breaking the spinner
|
||||
|
||||
|
||||
0.8.0 / 2017-12-31
|
||||
------------------
|
||||
|
||||
* Speedup reading spinners collection with simplejson
|
||||
|
||||
|
||||
0.7.1 / 2017-12-02
|
||||
------------------
|
||||
|
||||
* fix(#7): handling bytes sequences in ``Spinner.frames``
|
||||
|
||||
|
||||
0.7.0 / 2017-11-28
|
||||
------------------
|
||||
|
||||
* Reverse spinner support
|
||||
|
||||
|
||||
0.6.0 / 2017-11-26
|
||||
------------------
|
||||
|
||||
* Right spinner support
|
||||
|
||||
|
||||
0.5.0 / 2017-11-24
|
||||
------------------
|
||||
|
||||
* Colors support
|
||||
|
||||
|
||||
0.4.2 / 2017-11-17
|
||||
------------------
|
||||
|
||||
* RST vs PyPI episode 2
|
||||
|
||||
|
||||
0.4.1 / 2017-11-17
|
||||
------------------
|
||||
|
||||
* RST vs PyPI episode 1
|
||||
|
||||
|
||||
0.4.0 / 2017-11-17
|
||||
------------------
|
||||
|
||||
* Support for success and failure finalizers
|
||||
|
||||
|
||||
0.3.0 / 2017-11-14
|
||||
------------------
|
||||
|
||||
* Support for changing spinner properties on the fly
|
||||
|
||||
|
||||
0.2.0 / 2017-11-10
|
||||
------------------
|
||||
|
||||
* Support all spinners from `cli-spinners`_
|
||||
* API changes:
|
||||
- ``yaspin.spinner`` -> ``yaspin.yaspin``
|
||||
|
||||
|
||||
0.1.0 / 2017-10-31
|
||||
------------------
|
||||
|
||||
* First version
|
||||
|
||||
|
||||
.. _cli-spinners: https://github.com/sindresorhus/cli-spinners
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Pavlo Dmytrenko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('ruamel',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('ruamel', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('ruamel', [os.path.dirname(p)])));m = m or sys.modules.setdefault('ruamel', types.ModuleType('ruamel'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2022 Anthon van der Neut, Ruamel bvba
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2022 Anthon van der Neut, Ruamel bvba
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# coding: utf-8
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Dict, Any # NOQA
|
||||
|
||||
_package_data = dict(
|
||||
full_package_name='ruamel.yaml',
|
||||
version_info=(0, 17, 21),
|
||||
__version__='0.17.21',
|
||||
version_timestamp='2022-02-12 09:49:22',
|
||||
author='Anthon van der Neut',
|
||||
author_email='a.van.der.neut@ruamel.eu',
|
||||
description='ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order', # NOQA
|
||||
entry_points=None,
|
||||
since=2014,
|
||||
extras_require={
|
||||
':platform_python_implementation=="CPython" and python_version<"3.11"': ['ruamel.yaml.clib>=0.2.6'], # NOQA
|
||||
'jinja2': ['ruamel.yaml.jinja2>=0.2'],
|
||||
'docs': ['ryd'],
|
||||
},
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: Text Processing :: Markup',
|
||||
'Typing :: Typed',
|
||||
],
|
||||
keywords='yaml 1.2 parser round-trip preserve quotes order config',
|
||||
read_the_docs='yaml',
|
||||
supported=[(3, 5)], # minimum
|
||||
tox=dict(
|
||||
env='*f', # f for 3.5
|
||||
fl8excl='_test/lib',
|
||||
),
|
||||
# universal=True,
|
||||
python_requires='>=3',
|
||||
rtfd='yaml',
|
||||
) # type: Dict[Any, Any]
|
||||
|
||||
|
||||
version_info = _package_data['version_info']
|
||||
__version__ = _package_data['__version__']
|
||||
|
||||
try:
|
||||
from .cyaml import * # NOQA
|
||||
|
||||
__with_libyaml__ = True
|
||||
except (ImportError, ValueError): # for Jython
|
||||
__with_libyaml__ = False
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.main import * # NOQA
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# coding: utf-8
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List, Union, Optional, Iterator # NOQA
|
||||
|
||||
anchor_attrib = '_yaml_anchor'
|
||||
|
||||
|
||||
class Anchor:
|
||||
__slots__ = 'value', 'always_dump'
|
||||
attrib = anchor_attrib
|
||||
|
||||
def __init__(self):
|
||||
# type: () -> None
|
||||
self.value = None
|
||||
self.always_dump = False
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> Any
|
||||
ad = ', (always dump)' if self.always_dump else ""
|
||||
return 'Anchor({!r}{})'.format(self.value, ad)
|
||||
+1267
File diff suppressed because it is too large
Load Diff
Vendored
+268
@@ -0,0 +1,268 @@
|
||||
# coding: utf-8
|
||||
|
||||
# partially from package six by Benjamin Peterson
|
||||
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
import traceback
|
||||
from abc import abstractmethod
|
||||
import collections.abc
|
||||
|
||||
|
||||
# fmt: off
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List, Union, BinaryIO, IO, Text, Tuple # NOQA
|
||||
from typing import Optional # NOQA
|
||||
# fmt: on
|
||||
|
||||
_DEFAULT_YAML_VERSION = (1, 2)
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict # type: ignore
|
||||
|
||||
# to get the right name import ... as ordereddict doesn't do that
|
||||
|
||||
|
||||
class ordereddict(OrderedDict): # type: ignore
|
||||
if not hasattr(OrderedDict, 'insert'):
|
||||
|
||||
def insert(self, pos, key, value):
|
||||
# type: (int, Any, Any) -> None
|
||||
if pos >= len(self):
|
||||
self[key] = value
|
||||
return
|
||||
od = ordereddict()
|
||||
od.update(self)
|
||||
for k in od:
|
||||
del self[k]
|
||||
for index, old_key in enumerate(od):
|
||||
if pos == index:
|
||||
self[key] = value
|
||||
self[old_key] = od[old_key]
|
||||
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
|
||||
# replace with f-strings when 3.5 support is dropped
|
||||
# ft = '42'
|
||||
# assert _F('abc {ft!r}', ft=ft) == 'abc %r' % ft
|
||||
# 'abc %r' % ft -> _F('abc {ft!r}' -> f'abc {ft!r}'
|
||||
def _F(s, *superfluous, **kw):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
if superfluous:
|
||||
raise TypeError
|
||||
return s.format(**kw)
|
||||
|
||||
|
||||
StringIO = io.StringIO
|
||||
BytesIO = io.BytesIO
|
||||
|
||||
if False: # MYPY
|
||||
# StreamType = Union[BinaryIO, IO[str], IO[unicode], StringIO]
|
||||
# StreamType = Union[BinaryIO, IO[str], StringIO] # type: ignore
|
||||
StreamType = Any
|
||||
|
||||
StreamTextType = StreamType # Union[Text, StreamType]
|
||||
VersionType = Union[List[int], str, Tuple[int, int]]
|
||||
|
||||
builtins_module = 'builtins'
|
||||
|
||||
|
||||
def with_metaclass(meta, *bases):
|
||||
# type: (Any, Any) -> Any
|
||||
"""Create a base class with a metaclass."""
|
||||
return meta('NewBase', bases, {})
|
||||
|
||||
|
||||
DBG_TOKEN = 1
|
||||
DBG_EVENT = 2
|
||||
DBG_NODE = 4
|
||||
|
||||
|
||||
_debug = None # type: Optional[int]
|
||||
if 'RUAMELDEBUG' in os.environ:
|
||||
_debugx = os.environ.get('RUAMELDEBUG')
|
||||
if _debugx is None:
|
||||
_debug = 0
|
||||
else:
|
||||
_debug = int(_debugx)
|
||||
|
||||
|
||||
if bool(_debug):
|
||||
|
||||
class ObjectCounter:
|
||||
def __init__(self):
|
||||
# type: () -> None
|
||||
self.map = {} # type: Dict[Any, Any]
|
||||
|
||||
def __call__(self, k):
|
||||
# type: (Any) -> None
|
||||
self.map[k] = self.map.get(k, 0) + 1
|
||||
|
||||
def dump(self):
|
||||
# type: () -> None
|
||||
for k in sorted(self.map):
|
||||
sys.stdout.write('{} -> {}'.format(k, self.map[k]))
|
||||
|
||||
object_counter = ObjectCounter()
|
||||
|
||||
|
||||
# used from yaml util when testing
|
||||
def dbg(val=None):
|
||||
# type: (Any) -> Any
|
||||
global _debug
|
||||
if _debug is None:
|
||||
# set to true or false
|
||||
_debugx = os.environ.get('YAMLDEBUG')
|
||||
if _debugx is None:
|
||||
_debug = 0
|
||||
else:
|
||||
_debug = int(_debugx)
|
||||
if val is None:
|
||||
return _debug
|
||||
return _debug & val
|
||||
|
||||
|
||||
class Nprint:
|
||||
def __init__(self, file_name=None):
|
||||
# type: (Any) -> None
|
||||
self._max_print = None # type: Any
|
||||
self._count = None # type: Any
|
||||
self._file_name = file_name
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
# type: (Any, Any) -> None
|
||||
if not bool(_debug):
|
||||
return
|
||||
out = sys.stdout if self._file_name is None else open(self._file_name, 'a')
|
||||
dbgprint = print # to fool checking for print statements by dv utility
|
||||
kw1 = kw.copy()
|
||||
kw1['file'] = out
|
||||
dbgprint(*args, **kw1)
|
||||
out.flush()
|
||||
if self._max_print is not None:
|
||||
if self._count is None:
|
||||
self._count = self._max_print
|
||||
self._count -= 1
|
||||
if self._count == 0:
|
||||
dbgprint('forced exit\n')
|
||||
traceback.print_stack()
|
||||
out.flush()
|
||||
sys.exit(0)
|
||||
if self._file_name:
|
||||
out.close()
|
||||
|
||||
def set_max_print(self, i):
|
||||
# type: (int) -> None
|
||||
self._max_print = i
|
||||
self._count = None
|
||||
|
||||
def fp(self, mode='a'):
|
||||
# type: (str) -> Any
|
||||
out = sys.stdout if self._file_name is None else open(self._file_name, mode)
|
||||
return out
|
||||
|
||||
|
||||
nprint = Nprint()
|
||||
nprintf = Nprint('/var/tmp/ruamel.yaml.log')
|
||||
|
||||
# char checkers following production rules
|
||||
|
||||
|
||||
def check_namespace_char(ch):
|
||||
# type: (Any) -> bool
|
||||
if '\x21' <= ch <= '\x7E': # ! to ~
|
||||
return True
|
||||
if '\xA0' <= ch <= '\uD7FF':
|
||||
return True
|
||||
if ('\uE000' <= ch <= '\uFFFD') and ch != '\uFEFF': # excl. byte order mark
|
||||
return True
|
||||
if '\U00010000' <= ch <= '\U0010FFFF':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_anchorname_char(ch):
|
||||
# type: (Any) -> bool
|
||||
if ch in ',[]{}':
|
||||
return False
|
||||
return check_namespace_char(ch)
|
||||
|
||||
|
||||
def version_tnf(t1, t2=None):
|
||||
# type: (Any, Any) -> Any
|
||||
"""
|
||||
return True if ruamel version_info < t1, None if t2 is specified and bigger else False
|
||||
"""
|
||||
from pipenv.vendor.ruamel.yaml import version_info # NOQA
|
||||
|
||||
if version_info < t1:
|
||||
return True
|
||||
if t2 is not None and version_info < t2:
|
||||
return None
|
||||
return False
|
||||
|
||||
|
||||
class MutableSliceableSequence(collections.abc.MutableSequence): # type: ignore
|
||||
__slots__ = ()
|
||||
|
||||
def __getitem__(self, index):
|
||||
# type: (Any) -> Any
|
||||
if not isinstance(index, slice):
|
||||
return self.__getsingleitem__(index)
|
||||
return type(self)([self[i] for i in range(*index.indices(len(self)))]) # type: ignore
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
# type: (Any, Any) -> None
|
||||
if not isinstance(index, slice):
|
||||
return self.__setsingleitem__(index, value)
|
||||
assert iter(value)
|
||||
# nprint(index.start, index.stop, index.step, index.indices(len(self)))
|
||||
if index.step is None:
|
||||
del self[index.start : index.stop]
|
||||
for elem in reversed(value):
|
||||
self.insert(0 if index.start is None else index.start, elem)
|
||||
else:
|
||||
range_parms = index.indices(len(self))
|
||||
nr_assigned_items = (range_parms[1] - range_parms[0] - 1) // range_parms[2] + 1
|
||||
# need to test before changing, in case TypeError is caught
|
||||
if nr_assigned_items < len(value):
|
||||
raise TypeError(
|
||||
'too many elements in value {} < {}'.format(nr_assigned_items, len(value))
|
||||
)
|
||||
elif nr_assigned_items > len(value):
|
||||
raise TypeError(
|
||||
'not enough elements in value {} > {}'.format(
|
||||
nr_assigned_items, len(value)
|
||||
)
|
||||
)
|
||||
for idx, i in enumerate(range(*range_parms)):
|
||||
self[i] = value[idx]
|
||||
|
||||
def __delitem__(self, index):
|
||||
# type: (Any) -> None
|
||||
if not isinstance(index, slice):
|
||||
return self.__delsingleitem__(index)
|
||||
# nprint(index.start, index.stop, index.step, index.indices(len(self)))
|
||||
for i in reversed(range(*index.indices(len(self)))):
|
||||
del self[i]
|
||||
|
||||
@abstractmethod
|
||||
def __getsingleitem__(self, index):
|
||||
# type: (Any) -> Any
|
||||
raise IndexError
|
||||
|
||||
@abstractmethod
|
||||
def __setsingleitem__(self, index, value):
|
||||
# type: (Any, Any) -> None
|
||||
raise IndexError
|
||||
|
||||
@abstractmethod
|
||||
def __delsingleitem__(self, index):
|
||||
# type: (Any) -> None
|
||||
raise IndexError
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
# coding: utf-8
|
||||
|
||||
import warnings
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.error import MarkedYAMLError, ReusedAnchorWarning
|
||||
from pipenv.vendor.ruamel.yaml.compat import _F, nprint, nprintf # NOQA
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.events import (
|
||||
StreamStartEvent,
|
||||
StreamEndEvent,
|
||||
MappingStartEvent,
|
||||
MappingEndEvent,
|
||||
SequenceStartEvent,
|
||||
SequenceEndEvent,
|
||||
AliasEvent,
|
||||
ScalarEvent,
|
||||
)
|
||||
from pipenv.vendor.ruamel.yaml.nodes import MappingNode, ScalarNode, SequenceNode
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List # NOQA
|
||||
|
||||
__all__ = ['Composer', 'ComposerError']
|
||||
|
||||
|
||||
class ComposerError(MarkedYAMLError):
|
||||
pass
|
||||
|
||||
|
||||
class Composer:
|
||||
def __init__(self, loader=None):
|
||||
# type: (Any) -> None
|
||||
self.loader = loader
|
||||
if self.loader is not None and getattr(self.loader, '_composer', None) is None:
|
||||
self.loader._composer = self
|
||||
self.anchors = {} # type: Dict[Any, Any]
|
||||
|
||||
@property
|
||||
def parser(self):
|
||||
# type: () -> Any
|
||||
if hasattr(self.loader, 'typ'):
|
||||
self.loader.parser
|
||||
return self.loader._parser
|
||||
|
||||
@property
|
||||
def resolver(self):
|
||||
# type: () -> Any
|
||||
# assert self.loader._resolver is not None
|
||||
if hasattr(self.loader, 'typ'):
|
||||
self.loader.resolver
|
||||
return self.loader._resolver
|
||||
|
||||
def check_node(self):
|
||||
# type: () -> Any
|
||||
# Drop the STREAM-START event.
|
||||
if self.parser.check_event(StreamStartEvent):
|
||||
self.parser.get_event()
|
||||
|
||||
# If there are more documents available?
|
||||
return not self.parser.check_event(StreamEndEvent)
|
||||
|
||||
def get_node(self):
|
||||
# type: () -> Any
|
||||
# Get the root node of the next document.
|
||||
if not self.parser.check_event(StreamEndEvent):
|
||||
return self.compose_document()
|
||||
|
||||
def get_single_node(self):
|
||||
# type: () -> Any
|
||||
# Drop the STREAM-START event.
|
||||
self.parser.get_event()
|
||||
|
||||
# Compose a document if the stream is not empty.
|
||||
document = None # type: Any
|
||||
if not self.parser.check_event(StreamEndEvent):
|
||||
document = self.compose_document()
|
||||
|
||||
# Ensure that the stream contains no more documents.
|
||||
if not self.parser.check_event(StreamEndEvent):
|
||||
event = self.parser.get_event()
|
||||
raise ComposerError(
|
||||
'expected a single document in the stream',
|
||||
document.start_mark,
|
||||
'but found another document',
|
||||
event.start_mark,
|
||||
)
|
||||
|
||||
# Drop the STREAM-END event.
|
||||
self.parser.get_event()
|
||||
|
||||
return document
|
||||
|
||||
def compose_document(self):
|
||||
# type: (Any) -> Any
|
||||
# Drop the DOCUMENT-START event.
|
||||
self.parser.get_event()
|
||||
|
||||
# Compose the root node.
|
||||
node = self.compose_node(None, None)
|
||||
|
||||
# Drop the DOCUMENT-END event.
|
||||
self.parser.get_event()
|
||||
|
||||
self.anchors = {}
|
||||
return node
|
||||
|
||||
def return_alias(self, a):
|
||||
# type: (Any) -> Any
|
||||
return a
|
||||
|
||||
def compose_node(self, parent, index):
|
||||
# type: (Any, Any) -> Any
|
||||
if self.parser.check_event(AliasEvent):
|
||||
event = self.parser.get_event()
|
||||
alias = event.anchor
|
||||
if alias not in self.anchors:
|
||||
raise ComposerError(
|
||||
None,
|
||||
None,
|
||||
_F('found undefined alias {alias!r}', alias=alias),
|
||||
event.start_mark,
|
||||
)
|
||||
return self.return_alias(self.anchors[alias])
|
||||
event = self.parser.peek_event()
|
||||
anchor = event.anchor
|
||||
if anchor is not None: # have an anchor
|
||||
if anchor in self.anchors:
|
||||
# raise ComposerError(
|
||||
# "found duplicate anchor %r; first occurrence"
|
||||
# % (anchor), self.anchors[anchor].start_mark,
|
||||
# "second occurrence", event.start_mark)
|
||||
ws = (
|
||||
'\nfound duplicate anchor {!r}\nfirst occurrence {}\nsecond occurrence '
|
||||
'{}'.format((anchor), self.anchors[anchor].start_mark, event.start_mark)
|
||||
)
|
||||
warnings.warn(ws, ReusedAnchorWarning)
|
||||
self.resolver.descend_resolver(parent, index)
|
||||
if self.parser.check_event(ScalarEvent):
|
||||
node = self.compose_scalar_node(anchor)
|
||||
elif self.parser.check_event(SequenceStartEvent):
|
||||
node = self.compose_sequence_node(anchor)
|
||||
elif self.parser.check_event(MappingStartEvent):
|
||||
node = self.compose_mapping_node(anchor)
|
||||
self.resolver.ascend_resolver()
|
||||
return node
|
||||
|
||||
def compose_scalar_node(self, anchor):
|
||||
# type: (Any) -> Any
|
||||
event = self.parser.get_event()
|
||||
tag = event.tag
|
||||
if tag is None or tag == '!':
|
||||
tag = self.resolver.resolve(ScalarNode, event.value, event.implicit)
|
||||
node = ScalarNode(
|
||||
tag,
|
||||
event.value,
|
||||
event.start_mark,
|
||||
event.end_mark,
|
||||
style=event.style,
|
||||
comment=event.comment,
|
||||
anchor=anchor,
|
||||
)
|
||||
if anchor is not None:
|
||||
self.anchors[anchor] = node
|
||||
return node
|
||||
|
||||
def compose_sequence_node(self, anchor):
|
||||
# type: (Any) -> Any
|
||||
start_event = self.parser.get_event()
|
||||
tag = start_event.tag
|
||||
if tag is None or tag == '!':
|
||||
tag = self.resolver.resolve(SequenceNode, None, start_event.implicit)
|
||||
node = SequenceNode(
|
||||
tag,
|
||||
[],
|
||||
start_event.start_mark,
|
||||
None,
|
||||
flow_style=start_event.flow_style,
|
||||
comment=start_event.comment,
|
||||
anchor=anchor,
|
||||
)
|
||||
if anchor is not None:
|
||||
self.anchors[anchor] = node
|
||||
index = 0
|
||||
while not self.parser.check_event(SequenceEndEvent):
|
||||
node.value.append(self.compose_node(node, index))
|
||||
index += 1
|
||||
end_event = self.parser.get_event()
|
||||
if node.flow_style is True and end_event.comment is not None:
|
||||
if node.comment is not None:
|
||||
nprint(
|
||||
'Warning: unexpected end_event commment in sequence '
|
||||
'node {}'.format(node.flow_style)
|
||||
)
|
||||
node.comment = end_event.comment
|
||||
node.end_mark = end_event.end_mark
|
||||
self.check_end_doc_comment(end_event, node)
|
||||
return node
|
||||
|
||||
def compose_mapping_node(self, anchor):
|
||||
# type: (Any) -> Any
|
||||
start_event = self.parser.get_event()
|
||||
tag = start_event.tag
|
||||
if tag is None or tag == '!':
|
||||
tag = self.resolver.resolve(MappingNode, None, start_event.implicit)
|
||||
node = MappingNode(
|
||||
tag,
|
||||
[],
|
||||
start_event.start_mark,
|
||||
None,
|
||||
flow_style=start_event.flow_style,
|
||||
comment=start_event.comment,
|
||||
anchor=anchor,
|
||||
)
|
||||
if anchor is not None:
|
||||
self.anchors[anchor] = node
|
||||
while not self.parser.check_event(MappingEndEvent):
|
||||
# key_event = self.parser.peek_event()
|
||||
item_key = self.compose_node(node, None)
|
||||
# if item_key in node.value:
|
||||
# raise ComposerError("while composing a mapping",
|
||||
# start_event.start_mark,
|
||||
# "found duplicate key", key_event.start_mark)
|
||||
item_value = self.compose_node(node, item_key)
|
||||
# node.value[item_key] = item_value
|
||||
node.value.append((item_key, item_value))
|
||||
end_event = self.parser.get_event()
|
||||
if node.flow_style is True and end_event.comment is not None:
|
||||
node.comment = end_event.comment
|
||||
node.end_mark = end_event.end_mark
|
||||
self.check_end_doc_comment(end_event, node)
|
||||
return node
|
||||
|
||||
def check_end_doc_comment(self, end_event, node):
|
||||
# type: (Any, Any) -> None
|
||||
if end_event.comment and end_event.comment[1]:
|
||||
# pre comments on an end_event, no following to move to
|
||||
if node.comment is None:
|
||||
node.comment = [None, None]
|
||||
assert not isinstance(node, ScalarEvent)
|
||||
# this is a post comment on a mapping node, add as third element
|
||||
# in the list
|
||||
node.comment.append(end_event.comment[1])
|
||||
end_event.comment[1] = None
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# coding: utf-8
|
||||
|
||||
import warnings
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.util import configobj_walker as new_configobj_walker
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any # NOQA
|
||||
|
||||
|
||||
def configobj_walker(cfg):
|
||||
# type: (Any) -> Any
|
||||
warnings.warn('configobj_walker has moved to ruamel.util, please update your code')
|
||||
return new_configobj_walker(cfg)
|
||||
+1845
File diff suppressed because it is too large
Load Diff
Vendored
+183
@@ -0,0 +1,183 @@
|
||||
# coding: utf-8
|
||||
|
||||
from _ruamel_yaml import CParser, CEmitter # type: ignore
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.constructor import Constructor, BaseConstructor, SafeConstructor
|
||||
from pipenv.vendor.ruamel.yaml.representer import Representer, SafeRepresenter, BaseRepresenter
|
||||
from pipenv.vendor.ruamel.yaml.resolver import Resolver, BaseResolver
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Union, Optional # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.compat import StreamTextType, StreamType, VersionType # NOQA
|
||||
|
||||
__all__ = ['CBaseLoader', 'CSafeLoader', 'CLoader', 'CBaseDumper', 'CSafeDumper', 'CDumper']
|
||||
|
||||
|
||||
# this includes some hacks to solve the usage of resolver by lower level
|
||||
# parts of the parser
|
||||
|
||||
|
||||
class CBaseLoader(CParser, BaseConstructor, BaseResolver): # type: ignore
|
||||
def __init__(self, stream, version=None, preserve_quotes=None):
|
||||
# type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
|
||||
CParser.__init__(self, stream)
|
||||
self._parser = self._composer = self
|
||||
BaseConstructor.__init__(self, loader=self)
|
||||
BaseResolver.__init__(self, loadumper=self)
|
||||
# self.descend_resolver = self._resolver.descend_resolver
|
||||
# self.ascend_resolver = self._resolver.ascend_resolver
|
||||
# self.resolve = self._resolver.resolve
|
||||
|
||||
|
||||
class CSafeLoader(CParser, SafeConstructor, Resolver): # type: ignore
|
||||
def __init__(self, stream, version=None, preserve_quotes=None):
|
||||
# type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
|
||||
CParser.__init__(self, stream)
|
||||
self._parser = self._composer = self
|
||||
SafeConstructor.__init__(self, loader=self)
|
||||
Resolver.__init__(self, loadumper=self)
|
||||
# self.descend_resolver = self._resolver.descend_resolver
|
||||
# self.ascend_resolver = self._resolver.ascend_resolver
|
||||
# self.resolve = self._resolver.resolve
|
||||
|
||||
|
||||
class CLoader(CParser, Constructor, Resolver): # type: ignore
|
||||
def __init__(self, stream, version=None, preserve_quotes=None):
|
||||
# type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
|
||||
CParser.__init__(self, stream)
|
||||
self._parser = self._composer = self
|
||||
Constructor.__init__(self, loader=self)
|
||||
Resolver.__init__(self, loadumper=self)
|
||||
# self.descend_resolver = self._resolver.descend_resolver
|
||||
# self.ascend_resolver = self._resolver.ascend_resolver
|
||||
# self.resolve = self._resolver.resolve
|
||||
|
||||
|
||||
class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): # type: ignore
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
default_style=None,
|
||||
default_flow_style=None,
|
||||
canonical=None,
|
||||
indent=None,
|
||||
width=None,
|
||||
allow_unicode=None,
|
||||
line_break=None,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
block_seq_indent=None,
|
||||
top_level_colon_align=None,
|
||||
prefix_colon=None,
|
||||
):
|
||||
# type: (StreamType, Any, Any, Any, Optional[bool], Optional[int], Optional[int], Optional[bool], Any, Any, Optional[bool], Optional[bool], Any, Any, Any, Any, Any) -> None # NOQA
|
||||
CEmitter.__init__(
|
||||
self,
|
||||
stream,
|
||||
canonical=canonical,
|
||||
indent=indent,
|
||||
width=width,
|
||||
encoding=encoding,
|
||||
allow_unicode=allow_unicode,
|
||||
line_break=line_break,
|
||||
explicit_start=explicit_start,
|
||||
explicit_end=explicit_end,
|
||||
version=version,
|
||||
tags=tags,
|
||||
)
|
||||
self._emitter = self._serializer = self._representer = self
|
||||
BaseRepresenter.__init__(
|
||||
self,
|
||||
default_style=default_style,
|
||||
default_flow_style=default_flow_style,
|
||||
dumper=self,
|
||||
)
|
||||
BaseResolver.__init__(self, loadumper=self)
|
||||
|
||||
|
||||
class CSafeDumper(CEmitter, SafeRepresenter, Resolver): # type: ignore
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
default_style=None,
|
||||
default_flow_style=None,
|
||||
canonical=None,
|
||||
indent=None,
|
||||
width=None,
|
||||
allow_unicode=None,
|
||||
line_break=None,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
block_seq_indent=None,
|
||||
top_level_colon_align=None,
|
||||
prefix_colon=None,
|
||||
):
|
||||
# type: (StreamType, Any, Any, Any, Optional[bool], Optional[int], Optional[int], Optional[bool], Any, Any, Optional[bool], Optional[bool], Any, Any, Any, Any, Any) -> None # NOQA
|
||||
self._emitter = self._serializer = self._representer = self
|
||||
CEmitter.__init__(
|
||||
self,
|
||||
stream,
|
||||
canonical=canonical,
|
||||
indent=indent,
|
||||
width=width,
|
||||
encoding=encoding,
|
||||
allow_unicode=allow_unicode,
|
||||
line_break=line_break,
|
||||
explicit_start=explicit_start,
|
||||
explicit_end=explicit_end,
|
||||
version=version,
|
||||
tags=tags,
|
||||
)
|
||||
self._emitter = self._serializer = self._representer = self
|
||||
SafeRepresenter.__init__(
|
||||
self, default_style=default_style, default_flow_style=default_flow_style
|
||||
)
|
||||
Resolver.__init__(self)
|
||||
|
||||
|
||||
class CDumper(CEmitter, Representer, Resolver): # type: ignore
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
default_style=None,
|
||||
default_flow_style=None,
|
||||
canonical=None,
|
||||
indent=None,
|
||||
width=None,
|
||||
allow_unicode=None,
|
||||
line_break=None,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
block_seq_indent=None,
|
||||
top_level_colon_align=None,
|
||||
prefix_colon=None,
|
||||
):
|
||||
# type: (StreamType, Any, Any, Any, Optional[bool], Optional[int], Optional[int], Optional[bool], Any, Any, Optional[bool], Optional[bool], Any, Any, Any, Any, Any) -> None # NOQA
|
||||
CEmitter.__init__(
|
||||
self,
|
||||
stream,
|
||||
canonical=canonical,
|
||||
indent=indent,
|
||||
width=width,
|
||||
encoding=encoding,
|
||||
allow_unicode=allow_unicode,
|
||||
line_break=line_break,
|
||||
explicit_start=explicit_start,
|
||||
explicit_end=explicit_end,
|
||||
version=version,
|
||||
tags=tags,
|
||||
)
|
||||
self._emitter = self._serializer = self._representer = self
|
||||
Representer.__init__(
|
||||
self, default_style=default_style, default_flow_style=default_flow_style
|
||||
)
|
||||
Resolver.__init__(self)
|
||||
Vendored
+219
@@ -0,0 +1,219 @@
|
||||
# coding: utf-8
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.emitter import Emitter
|
||||
from pipenv.vendor.ruamel.yaml.serializer import Serializer
|
||||
from pipenv.vendor.ruamel.yaml.representer import (
|
||||
Representer,
|
||||
SafeRepresenter,
|
||||
BaseRepresenter,
|
||||
RoundTripRepresenter,
|
||||
)
|
||||
from pipenv.vendor.ruamel.yaml.resolver import Resolver, BaseResolver, VersionedResolver
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, List, Union, Optional # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.compat import StreamType, VersionType # NOQA
|
||||
|
||||
__all__ = ['BaseDumper', 'SafeDumper', 'Dumper', 'RoundTripDumper']
|
||||
|
||||
|
||||
class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver):
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
default_style=None,
|
||||
default_flow_style=None,
|
||||
canonical=None,
|
||||
indent=None,
|
||||
width=None,
|
||||
allow_unicode=None,
|
||||
line_break=None,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
block_seq_indent=None,
|
||||
top_level_colon_align=None,
|
||||
prefix_colon=None,
|
||||
):
|
||||
# type: (Any, StreamType, Any, Any, Optional[bool], Optional[int], Optional[int], Optional[bool], Any, Any, Optional[bool], Optional[bool], Any, Any, Any, Any, Any) -> None # NOQA
|
||||
Emitter.__init__(
|
||||
self,
|
||||
stream,
|
||||
canonical=canonical,
|
||||
indent=indent,
|
||||
width=width,
|
||||
allow_unicode=allow_unicode,
|
||||
line_break=line_break,
|
||||
block_seq_indent=block_seq_indent,
|
||||
dumper=self,
|
||||
)
|
||||
Serializer.__init__(
|
||||
self,
|
||||
encoding=encoding,
|
||||
explicit_start=explicit_start,
|
||||
explicit_end=explicit_end,
|
||||
version=version,
|
||||
tags=tags,
|
||||
dumper=self,
|
||||
)
|
||||
BaseRepresenter.__init__(
|
||||
self,
|
||||
default_style=default_style,
|
||||
default_flow_style=default_flow_style,
|
||||
dumper=self,
|
||||
)
|
||||
BaseResolver.__init__(self, loadumper=self)
|
||||
|
||||
|
||||
class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver):
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
default_style=None,
|
||||
default_flow_style=None,
|
||||
canonical=None,
|
||||
indent=None,
|
||||
width=None,
|
||||
allow_unicode=None,
|
||||
line_break=None,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
block_seq_indent=None,
|
||||
top_level_colon_align=None,
|
||||
prefix_colon=None,
|
||||
):
|
||||
# type: (StreamType, Any, Any, Optional[bool], Optional[int], Optional[int], Optional[bool], Any, Any, Optional[bool], Optional[bool], Any, Any, Any, Any, Any) -> None # NOQA
|
||||
Emitter.__init__(
|
||||
self,
|
||||
stream,
|
||||
canonical=canonical,
|
||||
indent=indent,
|
||||
width=width,
|
||||
allow_unicode=allow_unicode,
|
||||
line_break=line_break,
|
||||
block_seq_indent=block_seq_indent,
|
||||
dumper=self,
|
||||
)
|
||||
Serializer.__init__(
|
||||
self,
|
||||
encoding=encoding,
|
||||
explicit_start=explicit_start,
|
||||
explicit_end=explicit_end,
|
||||
version=version,
|
||||
tags=tags,
|
||||
dumper=self,
|
||||
)
|
||||
SafeRepresenter.__init__(
|
||||
self,
|
||||
default_style=default_style,
|
||||
default_flow_style=default_flow_style,
|
||||
dumper=self,
|
||||
)
|
||||
Resolver.__init__(self, loadumper=self)
|
||||
|
||||
|
||||
class Dumper(Emitter, Serializer, Representer, Resolver):
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
default_style=None,
|
||||
default_flow_style=None,
|
||||
canonical=None,
|
||||
indent=None,
|
||||
width=None,
|
||||
allow_unicode=None,
|
||||
line_break=None,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
block_seq_indent=None,
|
||||
top_level_colon_align=None,
|
||||
prefix_colon=None,
|
||||
):
|
||||
# type: (StreamType, Any, Any, Optional[bool], Optional[int], Optional[int], Optional[bool], Any, Any, Optional[bool], Optional[bool], Any, Any, Any, Any, Any) -> None # NOQA
|
||||
Emitter.__init__(
|
||||
self,
|
||||
stream,
|
||||
canonical=canonical,
|
||||
indent=indent,
|
||||
width=width,
|
||||
allow_unicode=allow_unicode,
|
||||
line_break=line_break,
|
||||
block_seq_indent=block_seq_indent,
|
||||
dumper=self,
|
||||
)
|
||||
Serializer.__init__(
|
||||
self,
|
||||
encoding=encoding,
|
||||
explicit_start=explicit_start,
|
||||
explicit_end=explicit_end,
|
||||
version=version,
|
||||
tags=tags,
|
||||
dumper=self,
|
||||
)
|
||||
Representer.__init__(
|
||||
self,
|
||||
default_style=default_style,
|
||||
default_flow_style=default_flow_style,
|
||||
dumper=self,
|
||||
)
|
||||
Resolver.__init__(self, loadumper=self)
|
||||
|
||||
|
||||
class RoundTripDumper(Emitter, Serializer, RoundTripRepresenter, VersionedResolver):
|
||||
def __init__(
|
||||
self,
|
||||
stream,
|
||||
default_style=None,
|
||||
default_flow_style=None,
|
||||
canonical=None,
|
||||
indent=None,
|
||||
width=None,
|
||||
allow_unicode=None,
|
||||
line_break=None,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
block_seq_indent=None,
|
||||
top_level_colon_align=None,
|
||||
prefix_colon=None,
|
||||
):
|
||||
# type: (StreamType, Any, Optional[bool], Optional[int], Optional[int], Optional[int], Optional[bool], Any, Any, Optional[bool], Optional[bool], Any, Any, Any, Any, Any) -> None # NOQA
|
||||
Emitter.__init__(
|
||||
self,
|
||||
stream,
|
||||
canonical=canonical,
|
||||
indent=indent,
|
||||
width=width,
|
||||
allow_unicode=allow_unicode,
|
||||
line_break=line_break,
|
||||
block_seq_indent=block_seq_indent,
|
||||
top_level_colon_align=top_level_colon_align,
|
||||
prefix_colon=prefix_colon,
|
||||
dumper=self,
|
||||
)
|
||||
Serializer.__init__(
|
||||
self,
|
||||
encoding=encoding,
|
||||
explicit_start=explicit_start,
|
||||
explicit_end=explicit_end,
|
||||
version=version,
|
||||
tags=tags,
|
||||
dumper=self,
|
||||
)
|
||||
RoundTripRepresenter.__init__(
|
||||
self,
|
||||
default_style=default_style,
|
||||
default_flow_style=default_flow_style,
|
||||
dumper=self,
|
||||
)
|
||||
VersionedResolver.__init__(self, loader=self)
|
||||
Vendored
+1772
File diff suppressed because it is too large
Load Diff
Vendored
+332
@@ -0,0 +1,332 @@
|
||||
# coding: utf-8
|
||||
|
||||
import warnings
|
||||
import textwrap
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.compat import _F
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List, Text # NOQA
|
||||
|
||||
|
||||
__all__ = [
|
||||
'FileMark',
|
||||
'StringMark',
|
||||
'CommentMark',
|
||||
'YAMLError',
|
||||
'MarkedYAMLError',
|
||||
'ReusedAnchorWarning',
|
||||
'UnsafeLoaderWarning',
|
||||
'MarkedYAMLWarning',
|
||||
'MarkedYAMLFutureWarning',
|
||||
]
|
||||
|
||||
|
||||
class StreamMark:
|
||||
__slots__ = 'name', 'index', 'line', 'column'
|
||||
|
||||
def __init__(self, name, index, line, column):
|
||||
# type: (Any, int, int, int) -> None
|
||||
self.name = name
|
||||
self.index = index
|
||||
self.line = line
|
||||
self.column = column
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> Any
|
||||
where = _F(
|
||||
' in "{sname!s}", line {sline1:d}, column {scolumn1:d}',
|
||||
sname=self.name,
|
||||
sline1=self.line + 1,
|
||||
scolumn1=self.column + 1,
|
||||
)
|
||||
return where
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (Any) -> bool
|
||||
if self.line != other.line or self.column != other.column:
|
||||
return False
|
||||
if self.name != other.name or self.index != other.index:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
# type: (Any) -> bool
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class FileMark(StreamMark):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class StringMark(StreamMark):
|
||||
__slots__ = 'name', 'index', 'line', 'column', 'buffer', 'pointer'
|
||||
|
||||
def __init__(self, name, index, line, column, buffer, pointer):
|
||||
# type: (Any, int, int, int, Any, Any) -> None
|
||||
StreamMark.__init__(self, name, index, line, column)
|
||||
self.buffer = buffer
|
||||
self.pointer = pointer
|
||||
|
||||
def get_snippet(self, indent=4, max_length=75):
|
||||
# type: (int, int) -> Any
|
||||
if self.buffer is None: # always False
|
||||
return None
|
||||
head = ""
|
||||
start = self.pointer
|
||||
while start > 0 and self.buffer[start - 1] not in '\0\r\n\x85\u2028\u2029':
|
||||
start -= 1
|
||||
if self.pointer - start > max_length / 2 - 1:
|
||||
head = ' ... '
|
||||
start += 5
|
||||
break
|
||||
tail = ""
|
||||
end = self.pointer
|
||||
while end < len(self.buffer) and self.buffer[end] not in '\0\r\n\x85\u2028\u2029':
|
||||
end += 1
|
||||
if end - self.pointer > max_length / 2 - 1:
|
||||
tail = ' ... '
|
||||
end -= 5
|
||||
break
|
||||
snippet = self.buffer[start:end]
|
||||
caret = '^'
|
||||
caret = '^ (line: {})'.format(self.line + 1)
|
||||
return (
|
||||
' ' * indent
|
||||
+ head
|
||||
+ snippet
|
||||
+ tail
|
||||
+ '\n'
|
||||
+ ' ' * (indent + self.pointer - start + len(head))
|
||||
+ caret
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> Any
|
||||
snippet = self.get_snippet()
|
||||
where = _F(
|
||||
' in "{sname!s}", line {sline1:d}, column {scolumn1:d}',
|
||||
sname=self.name,
|
||||
sline1=self.line + 1,
|
||||
scolumn1=self.column + 1,
|
||||
)
|
||||
if snippet is not None:
|
||||
where += ':\n' + snippet
|
||||
return where
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> Any
|
||||
snippet = self.get_snippet()
|
||||
where = _F(
|
||||
' in "{sname!s}", line {sline1:d}, column {scolumn1:d}',
|
||||
sname=self.name,
|
||||
sline1=self.line + 1,
|
||||
scolumn1=self.column + 1,
|
||||
)
|
||||
if snippet is not None:
|
||||
where += ':\n' + snippet
|
||||
return where
|
||||
|
||||
|
||||
class CommentMark:
|
||||
__slots__ = ('column',)
|
||||
|
||||
def __init__(self, column):
|
||||
# type: (Any) -> None
|
||||
self.column = column
|
||||
|
||||
|
||||
class YAMLError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MarkedYAMLError(YAMLError):
|
||||
def __init__(
|
||||
self,
|
||||
context=None,
|
||||
context_mark=None,
|
||||
problem=None,
|
||||
problem_mark=None,
|
||||
note=None,
|
||||
warn=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any) -> None
|
||||
self.context = context
|
||||
self.context_mark = context_mark
|
||||
self.problem = problem
|
||||
self.problem_mark = problem_mark
|
||||
self.note = note
|
||||
# warn is ignored
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> Any
|
||||
lines = [] # type: List[str]
|
||||
if self.context is not None:
|
||||
lines.append(self.context)
|
||||
if self.context_mark is not None and (
|
||||
self.problem is None
|
||||
or self.problem_mark is None
|
||||
or self.context_mark.name != self.problem_mark.name
|
||||
or self.context_mark.line != self.problem_mark.line
|
||||
or self.context_mark.column != self.problem_mark.column
|
||||
):
|
||||
lines.append(str(self.context_mark))
|
||||
if self.problem is not None:
|
||||
lines.append(self.problem)
|
||||
if self.problem_mark is not None:
|
||||
lines.append(str(self.problem_mark))
|
||||
if self.note is not None and self.note:
|
||||
note = textwrap.dedent(self.note)
|
||||
lines.append(note)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class YAMLStreamError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class YAMLWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class MarkedYAMLWarning(YAMLWarning):
|
||||
def __init__(
|
||||
self,
|
||||
context=None,
|
||||
context_mark=None,
|
||||
problem=None,
|
||||
problem_mark=None,
|
||||
note=None,
|
||||
warn=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any) -> None
|
||||
self.context = context
|
||||
self.context_mark = context_mark
|
||||
self.problem = problem
|
||||
self.problem_mark = problem_mark
|
||||
self.note = note
|
||||
self.warn = warn
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> Any
|
||||
lines = [] # type: List[str]
|
||||
if self.context is not None:
|
||||
lines.append(self.context)
|
||||
if self.context_mark is not None and (
|
||||
self.problem is None
|
||||
or self.problem_mark is None
|
||||
or self.context_mark.name != self.problem_mark.name
|
||||
or self.context_mark.line != self.problem_mark.line
|
||||
or self.context_mark.column != self.problem_mark.column
|
||||
):
|
||||
lines.append(str(self.context_mark))
|
||||
if self.problem is not None:
|
||||
lines.append(self.problem)
|
||||
if self.problem_mark is not None:
|
||||
lines.append(str(self.problem_mark))
|
||||
if self.note is not None and self.note:
|
||||
note = textwrap.dedent(self.note)
|
||||
lines.append(note)
|
||||
if self.warn is not None and self.warn:
|
||||
warn = textwrap.dedent(self.warn)
|
||||
lines.append(warn)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class ReusedAnchorWarning(YAMLWarning):
|
||||
pass
|
||||
|
||||
|
||||
class UnsafeLoaderWarning(YAMLWarning):
|
||||
text = """
|
||||
The default 'Loader' for 'load(stream)' without further arguments can be unsafe.
|
||||
Use 'load(stream, Loader=ruamel.yaml.Loader)' explicitly if that is OK.
|
||||
Alternatively include the following in your code:
|
||||
|
||||
import warnings
|
||||
warnings.simplefilter('ignore', ruamel.error.UnsafeLoaderWarning)
|
||||
|
||||
In most other cases you should consider using 'safe_load(stream)'"""
|
||||
pass
|
||||
|
||||
|
||||
warnings.simplefilter('once', UnsafeLoaderWarning)
|
||||
|
||||
|
||||
class MantissaNoDotYAML1_1Warning(YAMLWarning):
|
||||
def __init__(self, node, flt_str):
|
||||
# type: (Any, Any) -> None
|
||||
self.node = node
|
||||
self.flt = flt_str
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> Any
|
||||
line = self.node.start_mark.line
|
||||
col = self.node.start_mark.column
|
||||
return """
|
||||
In YAML 1.1 floating point values should have a dot ('.') in their mantissa.
|
||||
See the Floating-Point Language-Independent Type for YAML™ Version 1.1 specification
|
||||
( http://yaml.org/type/float.html ). This dot is not required for JSON nor for YAML 1.2
|
||||
|
||||
Correct your float: "{}" on line: {}, column: {}
|
||||
|
||||
or alternatively include the following in your code:
|
||||
|
||||
import warnings
|
||||
warnings.simplefilter('ignore', ruamel.error.MantissaNoDotYAML1_1Warning)
|
||||
|
||||
""".format(
|
||||
self.flt, line, col
|
||||
)
|
||||
|
||||
|
||||
warnings.simplefilter('once', MantissaNoDotYAML1_1Warning)
|
||||
|
||||
|
||||
class YAMLFutureWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class MarkedYAMLFutureWarning(YAMLFutureWarning):
|
||||
def __init__(
|
||||
self,
|
||||
context=None,
|
||||
context_mark=None,
|
||||
problem=None,
|
||||
problem_mark=None,
|
||||
note=None,
|
||||
warn=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any) -> None
|
||||
self.context = context
|
||||
self.context_mark = context_mark
|
||||
self.problem = problem
|
||||
self.problem_mark = problem_mark
|
||||
self.note = note
|
||||
self.warn = warn
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> Any
|
||||
lines = [] # type: List[str]
|
||||
if self.context is not None:
|
||||
lines.append(self.context)
|
||||
|
||||
if self.context_mark is not None and (
|
||||
self.problem is None
|
||||
or self.problem_mark is None
|
||||
or self.context_mark.name != self.problem_mark.name
|
||||
or self.context_mark.line != self.problem_mark.line
|
||||
or self.context_mark.column != self.problem_mark.column
|
||||
):
|
||||
lines.append(str(self.context_mark))
|
||||
if self.problem is not None:
|
||||
lines.append(self.problem)
|
||||
if self.problem_mark is not None:
|
||||
lines.append(str(self.problem_mark))
|
||||
if self.note is not None and self.note:
|
||||
note = textwrap.dedent(self.note)
|
||||
lines.append(note)
|
||||
if self.warn is not None and self.warn:
|
||||
warn = textwrap.dedent(self.warn)
|
||||
lines.append(warn)
|
||||
return '\n'.join(lines)
|
||||
Vendored
+196
@@ -0,0 +1,196 @@
|
||||
# coding: utf-8
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.compat import _F
|
||||
|
||||
# Abstract classes.
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List # NOQA
|
||||
|
||||
SHOW_LINES = False
|
||||
|
||||
|
||||
def CommentCheck():
|
||||
# type: () -> None
|
||||
pass
|
||||
|
||||
|
||||
class Event:
|
||||
__slots__ = 'start_mark', 'end_mark', 'comment'
|
||||
|
||||
def __init__(self, start_mark=None, end_mark=None, comment=CommentCheck):
|
||||
# type: (Any, Any, Any) -> None
|
||||
self.start_mark = start_mark
|
||||
self.end_mark = end_mark
|
||||
# assert comment is not CommentCheck
|
||||
if comment is CommentCheck:
|
||||
comment = None
|
||||
self.comment = comment
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> Any
|
||||
if True:
|
||||
arguments = []
|
||||
if hasattr(self, 'value'):
|
||||
# if you use repr(getattr(self, 'value')) then flake8 complains about
|
||||
# abuse of getattr with a constant. When you change to self.value
|
||||
# then mypy throws an error
|
||||
arguments.append(repr(self.value)) # type: ignore
|
||||
for key in ['anchor', 'tag', 'implicit', 'flow_style', 'style']:
|
||||
v = getattr(self, key, None)
|
||||
if v is not None:
|
||||
arguments.append(_F('{key!s}={v!r}', key=key, v=v))
|
||||
if self.comment not in [None, CommentCheck]:
|
||||
arguments.append('comment={!r}'.format(self.comment))
|
||||
if SHOW_LINES:
|
||||
arguments.append(
|
||||
'({}:{}/{}:{})'.format(
|
||||
self.start_mark.line,
|
||||
self.start_mark.column,
|
||||
self.end_mark.line,
|
||||
self.end_mark.column,
|
||||
)
|
||||
)
|
||||
arguments = ', '.join(arguments) # type: ignore
|
||||
else:
|
||||
attributes = [
|
||||
key
|
||||
for key in ['anchor', 'tag', 'implicit', 'value', 'flow_style', 'style']
|
||||
if hasattr(self, key)
|
||||
]
|
||||
arguments = ', '.join(
|
||||
[_F('{k!s}={attr!r}', k=key, attr=getattr(self, key)) for key in attributes]
|
||||
)
|
||||
if self.comment not in [None, CommentCheck]:
|
||||
arguments += ', comment={!r}'.format(self.comment)
|
||||
return _F(
|
||||
'{self_class_name!s}({arguments!s})',
|
||||
self_class_name=self.__class__.__name__,
|
||||
arguments=arguments,
|
||||
)
|
||||
|
||||
|
||||
class NodeEvent(Event):
|
||||
__slots__ = ('anchor',)
|
||||
|
||||
def __init__(self, anchor, start_mark=None, end_mark=None, comment=None):
|
||||
# type: (Any, Any, Any, Any) -> None
|
||||
Event.__init__(self, start_mark, end_mark, comment)
|
||||
self.anchor = anchor
|
||||
|
||||
|
||||
class CollectionStartEvent(NodeEvent):
|
||||
__slots__ = 'tag', 'implicit', 'flow_style', 'nr_items'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
anchor,
|
||||
tag,
|
||||
implicit,
|
||||
start_mark=None,
|
||||
end_mark=None,
|
||||
flow_style=None,
|
||||
comment=None,
|
||||
nr_items=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any, Any, Optional[int]) -> None
|
||||
NodeEvent.__init__(self, anchor, start_mark, end_mark, comment)
|
||||
self.tag = tag
|
||||
self.implicit = implicit
|
||||
self.flow_style = flow_style
|
||||
self.nr_items = nr_items
|
||||
|
||||
|
||||
class CollectionEndEvent(Event):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
# Implementations.
|
||||
|
||||
|
||||
class StreamStartEvent(Event):
|
||||
__slots__ = ('encoding',)
|
||||
|
||||
def __init__(self, start_mark=None, end_mark=None, encoding=None, comment=None):
|
||||
# type: (Any, Any, Any, Any) -> None
|
||||
Event.__init__(self, start_mark, end_mark, comment)
|
||||
self.encoding = encoding
|
||||
|
||||
|
||||
class StreamEndEvent(Event):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class DocumentStartEvent(Event):
|
||||
__slots__ = 'explicit', 'version', 'tags'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_mark=None,
|
||||
end_mark=None,
|
||||
explicit=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
comment=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any) -> None
|
||||
Event.__init__(self, start_mark, end_mark, comment)
|
||||
self.explicit = explicit
|
||||
self.version = version
|
||||
self.tags = tags
|
||||
|
||||
|
||||
class DocumentEndEvent(Event):
|
||||
__slots__ = ('explicit',)
|
||||
|
||||
def __init__(self, start_mark=None, end_mark=None, explicit=None, comment=None):
|
||||
# type: (Any, Any, Any, Any) -> None
|
||||
Event.__init__(self, start_mark, end_mark, comment)
|
||||
self.explicit = explicit
|
||||
|
||||
|
||||
class AliasEvent(NodeEvent):
|
||||
__slots__ = 'style'
|
||||
|
||||
def __init__(self, anchor, start_mark=None, end_mark=None, style=None, comment=None):
|
||||
# type: (Any, Any, Any, Any, Any) -> None
|
||||
NodeEvent.__init__(self, anchor, start_mark, end_mark, comment)
|
||||
self.style = style
|
||||
|
||||
|
||||
class ScalarEvent(NodeEvent):
|
||||
__slots__ = 'tag', 'implicit', 'value', 'style'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
anchor,
|
||||
tag,
|
||||
implicit,
|
||||
value,
|
||||
start_mark=None,
|
||||
end_mark=None,
|
||||
style=None,
|
||||
comment=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any, Any, Any) -> None
|
||||
NodeEvent.__init__(self, anchor, start_mark, end_mark, comment)
|
||||
self.tag = tag
|
||||
self.implicit = implicit
|
||||
self.value = value
|
||||
self.style = style
|
||||
|
||||
|
||||
class SequenceStartEvent(CollectionStartEvent):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class SequenceEndEvent(CollectionEndEvent):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class MappingStartEvent(CollectionStartEvent):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class MappingEndEvent(CollectionEndEvent):
|
||||
__slots__ = ()
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
# coding: utf-8
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.reader import Reader
|
||||
from pipenv.vendor.ruamel.yaml.scanner import Scanner, RoundTripScanner
|
||||
from pipenv.vendor.ruamel.yaml.parser import Parser, RoundTripParser
|
||||
from pipenv.vendor.ruamel.yaml.composer import Composer
|
||||
from pipenv.vendor.ruamel.yaml.constructor import (
|
||||
BaseConstructor,
|
||||
SafeConstructor,
|
||||
Constructor,
|
||||
RoundTripConstructor,
|
||||
)
|
||||
from pipenv.vendor.ruamel.yaml.resolver import VersionedResolver
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, List, Union, Optional # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.compat import StreamTextType, VersionType # NOQA
|
||||
|
||||
__all__ = ['BaseLoader', 'SafeLoader', 'Loader', 'RoundTripLoader']
|
||||
|
||||
|
||||
class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, VersionedResolver):
|
||||
def __init__(self, stream, version=None, preserve_quotes=None):
|
||||
# type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
|
||||
self.comment_handling = None
|
||||
Reader.__init__(self, stream, loader=self)
|
||||
Scanner.__init__(self, loader=self)
|
||||
Parser.__init__(self, loader=self)
|
||||
Composer.__init__(self, loader=self)
|
||||
BaseConstructor.__init__(self, loader=self)
|
||||
VersionedResolver.__init__(self, version, loader=self)
|
||||
|
||||
|
||||
class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, VersionedResolver):
|
||||
def __init__(self, stream, version=None, preserve_quotes=None):
|
||||
# type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
|
||||
self.comment_handling = None
|
||||
Reader.__init__(self, stream, loader=self)
|
||||
Scanner.__init__(self, loader=self)
|
||||
Parser.__init__(self, loader=self)
|
||||
Composer.__init__(self, loader=self)
|
||||
SafeConstructor.__init__(self, loader=self)
|
||||
VersionedResolver.__init__(self, version, loader=self)
|
||||
|
||||
|
||||
class Loader(Reader, Scanner, Parser, Composer, Constructor, VersionedResolver):
|
||||
def __init__(self, stream, version=None, preserve_quotes=None):
|
||||
# type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
|
||||
self.comment_handling = None
|
||||
Reader.__init__(self, stream, loader=self)
|
||||
Scanner.__init__(self, loader=self)
|
||||
Parser.__init__(self, loader=self)
|
||||
Composer.__init__(self, loader=self)
|
||||
Constructor.__init__(self, loader=self)
|
||||
VersionedResolver.__init__(self, version, loader=self)
|
||||
|
||||
|
||||
class RoundTripLoader(
|
||||
Reader,
|
||||
RoundTripScanner,
|
||||
RoundTripParser,
|
||||
Composer,
|
||||
RoundTripConstructor,
|
||||
VersionedResolver,
|
||||
):
|
||||
def __init__(self, stream, version=None, preserve_quotes=None):
|
||||
# type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None
|
||||
# self.reader = Reader.__init__(self, stream)
|
||||
self.comment_handling = None # issue 385
|
||||
Reader.__init__(self, stream, loader=self)
|
||||
RoundTripScanner.__init__(self, loader=self)
|
||||
RoundTripParser.__init__(self, loader=self)
|
||||
Composer.__init__(self, loader=self)
|
||||
RoundTripConstructor.__init__(self, preserve_quotes=preserve_quotes, loader=self)
|
||||
VersionedResolver.__init__(self, version, loader=self)
|
||||
Vendored
+1667
File diff suppressed because it is too large
Load Diff
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
# coding: utf-8
|
||||
|
||||
import sys
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.compat import _F
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Dict, Any, Text # NOQA
|
||||
|
||||
|
||||
class Node:
|
||||
__slots__ = 'tag', 'value', 'start_mark', 'end_mark', 'comment', 'anchor'
|
||||
|
||||
def __init__(self, tag, value, start_mark, end_mark, comment=None, anchor=None):
|
||||
# type: (Any, Any, Any, Any, Any, Any) -> None
|
||||
self.tag = tag
|
||||
self.value = value
|
||||
self.start_mark = start_mark
|
||||
self.end_mark = end_mark
|
||||
self.comment = comment
|
||||
self.anchor = anchor
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> Any
|
||||
value = self.value
|
||||
# if isinstance(value, list):
|
||||
# if len(value) == 0:
|
||||
# value = '<empty>'
|
||||
# elif len(value) == 1:
|
||||
# value = '<1 item>'
|
||||
# else:
|
||||
# value = f'<{len(value)} items>'
|
||||
# else:
|
||||
# if len(value) > 75:
|
||||
# value = repr(value[:70]+' ... ')
|
||||
# else:
|
||||
# value = repr(value)
|
||||
value = repr(value)
|
||||
return _F(
|
||||
'{class_name!s}(tag={self_tag!r}, value={value!s})',
|
||||
class_name=self.__class__.__name__,
|
||||
self_tag=self.tag,
|
||||
value=value,
|
||||
)
|
||||
|
||||
def dump(self, indent=0):
|
||||
# type: (int) -> None
|
||||
if isinstance(self.value, str):
|
||||
sys.stdout.write(
|
||||
'{}{}(tag={!r}, value={!r})\n'.format(
|
||||
' ' * indent, self.__class__.__name__, self.tag, self.value
|
||||
)
|
||||
)
|
||||
if self.comment:
|
||||
sys.stdout.write(' {}comment: {})\n'.format(' ' * indent, self.comment))
|
||||
return
|
||||
sys.stdout.write(
|
||||
'{}{}(tag={!r})\n'.format(' ' * indent, self.__class__.__name__, self.tag)
|
||||
)
|
||||
if self.comment:
|
||||
sys.stdout.write(' {}comment: {})\n'.format(' ' * indent, self.comment))
|
||||
for v in self.value:
|
||||
if isinstance(v, tuple):
|
||||
for v1 in v:
|
||||
v1.dump(indent + 1)
|
||||
elif isinstance(v, Node):
|
||||
v.dump(indent + 1)
|
||||
else:
|
||||
sys.stdout.write('Node value type? {}\n'.format(type(v)))
|
||||
|
||||
|
||||
class ScalarNode(Node):
|
||||
"""
|
||||
styles:
|
||||
? -> set() ? key, no value
|
||||
" -> double quoted
|
||||
' -> single quoted
|
||||
| -> literal style
|
||||
> -> folding style
|
||||
"""
|
||||
|
||||
__slots__ = ('style',)
|
||||
id = 'scalar'
|
||||
|
||||
def __init__(
|
||||
self, tag, value, start_mark=None, end_mark=None, style=None, comment=None, anchor=None
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any, Any) -> None
|
||||
Node.__init__(self, tag, value, start_mark, end_mark, comment=comment, anchor=anchor)
|
||||
self.style = style
|
||||
|
||||
|
||||
class CollectionNode(Node):
|
||||
__slots__ = ('flow_style',)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tag,
|
||||
value,
|
||||
start_mark=None,
|
||||
end_mark=None,
|
||||
flow_style=None,
|
||||
comment=None,
|
||||
anchor=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any, Any) -> None
|
||||
Node.__init__(self, tag, value, start_mark, end_mark, comment=comment)
|
||||
self.flow_style = flow_style
|
||||
self.anchor = anchor
|
||||
|
||||
|
||||
class SequenceNode(CollectionNode):
|
||||
__slots__ = ()
|
||||
id = 'sequence'
|
||||
|
||||
|
||||
class MappingNode(CollectionNode):
|
||||
__slots__ = ('merge',)
|
||||
id = 'mapping'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tag,
|
||||
value,
|
||||
start_mark=None,
|
||||
end_mark=None,
|
||||
flow_style=None,
|
||||
comment=None,
|
||||
anchor=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any, Any) -> None
|
||||
CollectionNode.__init__(
|
||||
self, tag, value, start_mark, end_mark, flow_style, comment, anchor
|
||||
)
|
||||
self.merge = None
|
||||
Vendored
+884
@@ -0,0 +1,884 @@
|
||||
# coding: utf-8
|
||||
|
||||
# The following YAML grammar is LL(1) and is parsed by a recursive descent
|
||||
# parser.
|
||||
#
|
||||
# stream ::= STREAM-START implicit_document? explicit_document*
|
||||
# STREAM-END
|
||||
# implicit_document ::= block_node DOCUMENT-END*
|
||||
# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
# block_node_or_indentless_sequence ::=
|
||||
# ALIAS
|
||||
# | properties (block_content |
|
||||
# indentless_block_sequence)?
|
||||
# | block_content
|
||||
# | indentless_block_sequence
|
||||
# block_node ::= ALIAS
|
||||
# | properties block_content?
|
||||
# | block_content
|
||||
# flow_node ::= ALIAS
|
||||
# | properties flow_content?
|
||||
# | flow_content
|
||||
# properties ::= TAG ANCHOR? | ANCHOR TAG?
|
||||
# block_content ::= block_collection | flow_collection | SCALAR
|
||||
# flow_content ::= flow_collection | SCALAR
|
||||
# block_collection ::= block_sequence | block_mapping
|
||||
# flow_collection ::= flow_sequence | flow_mapping
|
||||
# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)*
|
||||
# BLOCK-END
|
||||
# indentless_sequence ::= (BLOCK-ENTRY block_node?)+
|
||||
# block_mapping ::= BLOCK-MAPPING_START
|
||||
# ((KEY block_node_or_indentless_sequence?)?
|
||||
# (VALUE block_node_or_indentless_sequence?)?)*
|
||||
# BLOCK-END
|
||||
# flow_sequence ::= FLOW-SEQUENCE-START
|
||||
# (flow_sequence_entry FLOW-ENTRY)*
|
||||
# flow_sequence_entry?
|
||||
# FLOW-SEQUENCE-END
|
||||
# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
# flow_mapping ::= FLOW-MAPPING-START
|
||||
# (flow_mapping_entry FLOW-ENTRY)*
|
||||
# flow_mapping_entry?
|
||||
# FLOW-MAPPING-END
|
||||
# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
#
|
||||
# FIRST sets:
|
||||
#
|
||||
# stream: { STREAM-START <}
|
||||
# explicit_document: { DIRECTIVE DOCUMENT-START }
|
||||
# implicit_document: FIRST(block_node)
|
||||
# block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START
|
||||
# BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
||||
# flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
||||
# block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START
|
||||
# FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR }
|
||||
# flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR }
|
||||
# block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START }
|
||||
# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
||||
# block_sequence: { BLOCK-SEQUENCE-START }
|
||||
# block_mapping: { BLOCK-MAPPING-START }
|
||||
# block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR
|
||||
# BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START
|
||||
# FLOW-MAPPING-START BLOCK-ENTRY }
|
||||
# indentless_sequence: { ENTRY }
|
||||
# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START }
|
||||
# flow_sequence: { FLOW-SEQUENCE-START }
|
||||
# flow_mapping: { FLOW-MAPPING-START }
|
||||
# flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START
|
||||
# FLOW-MAPPING-START KEY }
|
||||
# flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START
|
||||
# FLOW-MAPPING-START KEY }
|
||||
|
||||
# need to have full path with import, as pkg_resources tries to load parser.py in __init__.py
|
||||
# only to not do anything with the package afterwards
|
||||
# and for Jython too
|
||||
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.error import MarkedYAMLError
|
||||
from pipenv.vendor.ruamel.yaml.tokens import * # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.events import * # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.scanner import Scanner, RoundTripScanner, ScannerError # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.scanner import BlankLineComment
|
||||
from pipenv.vendor.ruamel.yaml.comments import C_PRE, C_POST, C_SPLIT_ON_FIRST_BLANK
|
||||
from pipenv.vendor.ruamel.yaml.compat import _F, nprint, nprintf # NOQA
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List, Optional # NOQA
|
||||
|
||||
__all__ = ['Parser', 'RoundTripParser', 'ParserError']
|
||||
|
||||
|
||||
def xprintf(*args, **kw):
|
||||
# type: (Any, Any) -> Any
|
||||
return nprintf(*args, **kw)
|
||||
pass
|
||||
|
||||
|
||||
class ParserError(MarkedYAMLError):
|
||||
pass
|
||||
|
||||
|
||||
class Parser:
|
||||
# Since writing a recursive-descendant parser is a straightforward task, we
|
||||
# do not give many comments here.
|
||||
|
||||
DEFAULT_TAGS = {'!': '!', '!!': 'tag:yaml.org,2002:'}
|
||||
|
||||
def __init__(self, loader):
|
||||
# type: (Any) -> None
|
||||
self.loader = loader
|
||||
if self.loader is not None and getattr(self.loader, '_parser', None) is None:
|
||||
self.loader._parser = self
|
||||
self.reset_parser()
|
||||
|
||||
def reset_parser(self):
|
||||
# type: () -> None
|
||||
# Reset the state attributes (to clear self-references)
|
||||
self.current_event = self.last_event = None
|
||||
self.tag_handles = {} # type: Dict[Any, Any]
|
||||
self.states = [] # type: List[Any]
|
||||
self.marks = [] # type: List[Any]
|
||||
self.state = self.parse_stream_start # type: Any
|
||||
|
||||
def dispose(self):
|
||||
# type: () -> None
|
||||
self.reset_parser()
|
||||
|
||||
@property
|
||||
def scanner(self):
|
||||
# type: () -> Any
|
||||
if hasattr(self.loader, 'typ'):
|
||||
return self.loader.scanner
|
||||
return self.loader._scanner
|
||||
|
||||
@property
|
||||
def resolver(self):
|
||||
# type: () -> Any
|
||||
if hasattr(self.loader, 'typ'):
|
||||
return self.loader.resolver
|
||||
return self.loader._resolver
|
||||
|
||||
def check_event(self, *choices):
|
||||
# type: (Any) -> bool
|
||||
# Check the type of the next event.
|
||||
if self.current_event is None:
|
||||
if self.state:
|
||||
self.current_event = self.state()
|
||||
if self.current_event is not None:
|
||||
if not choices:
|
||||
return True
|
||||
for choice in choices:
|
||||
if isinstance(self.current_event, choice):
|
||||
return True
|
||||
return False
|
||||
|
||||
def peek_event(self):
|
||||
# type: () -> Any
|
||||
# Get the next event.
|
||||
if self.current_event is None:
|
||||
if self.state:
|
||||
self.current_event = self.state()
|
||||
return self.current_event
|
||||
|
||||
def get_event(self):
|
||||
# type: () -> Any
|
||||
# Get the next event and proceed further.
|
||||
if self.current_event is None:
|
||||
if self.state:
|
||||
self.current_event = self.state()
|
||||
# assert self.current_event is not None
|
||||
# if self.current_event.end_mark.line != self.peek_event().start_mark.line:
|
||||
xprintf('get_event', repr(self.current_event), self.peek_event().start_mark.line)
|
||||
self.last_event = value = self.current_event
|
||||
self.current_event = None
|
||||
return value
|
||||
|
||||
# stream ::= STREAM-START implicit_document? explicit_document*
|
||||
# STREAM-END
|
||||
# implicit_document ::= block_node DOCUMENT-END*
|
||||
# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
|
||||
def parse_stream_start(self):
|
||||
# type: () -> Any
|
||||
# Parse the stream start.
|
||||
token = self.scanner.get_token()
|
||||
self.move_token_comment(token)
|
||||
event = StreamStartEvent(token.start_mark, token.end_mark, encoding=token.encoding)
|
||||
|
||||
# Prepare the next state.
|
||||
self.state = self.parse_implicit_document_start
|
||||
|
||||
return event
|
||||
|
||||
def parse_implicit_document_start(self):
|
||||
# type: () -> Any
|
||||
# Parse an implicit document.
|
||||
if not self.scanner.check_token(DirectiveToken, DocumentStartToken, StreamEndToken):
|
||||
self.tag_handles = self.DEFAULT_TAGS
|
||||
token = self.scanner.peek_token()
|
||||
start_mark = end_mark = token.start_mark
|
||||
event = DocumentStartEvent(start_mark, end_mark, explicit=False)
|
||||
|
||||
# Prepare the next state.
|
||||
self.states.append(self.parse_document_end)
|
||||
self.state = self.parse_block_node
|
||||
|
||||
return event
|
||||
|
||||
else:
|
||||
return self.parse_document_start()
|
||||
|
||||
def parse_document_start(self):
|
||||
# type: () -> Any
|
||||
# Parse any extra document end indicators.
|
||||
while self.scanner.check_token(DocumentEndToken):
|
||||
self.scanner.get_token()
|
||||
# Parse an explicit document.
|
||||
if not self.scanner.check_token(StreamEndToken):
|
||||
version, tags = self.process_directives()
|
||||
if not self.scanner.check_token(DocumentStartToken):
|
||||
raise ParserError(
|
||||
None,
|
||||
None,
|
||||
_F(
|
||||
"expected '<document start>', but found {pt!r}",
|
||||
pt=self.scanner.peek_token().id,
|
||||
),
|
||||
self.scanner.peek_token().start_mark,
|
||||
)
|
||||
token = self.scanner.get_token()
|
||||
start_mark = token.start_mark
|
||||
end_mark = token.end_mark
|
||||
# if self.loader is not None and \
|
||||
# end_mark.line != self.scanner.peek_token().start_mark.line:
|
||||
# self.loader.scalar_after_indicator = False
|
||||
event = DocumentStartEvent(
|
||||
start_mark, end_mark, explicit=True, version=version, tags=tags,
|
||||
comment=token.comment
|
||||
) # type: Any
|
||||
self.states.append(self.parse_document_end)
|
||||
self.state = self.parse_document_content
|
||||
else:
|
||||
# Parse the end of the stream.
|
||||
token = self.scanner.get_token()
|
||||
event = StreamEndEvent(token.start_mark, token.end_mark, comment=token.comment)
|
||||
assert not self.states
|
||||
assert not self.marks
|
||||
self.state = None
|
||||
return event
|
||||
|
||||
def parse_document_end(self):
|
||||
# type: () -> Any
|
||||
# Parse the document end.
|
||||
token = self.scanner.peek_token()
|
||||
start_mark = end_mark = token.start_mark
|
||||
explicit = False
|
||||
if self.scanner.check_token(DocumentEndToken):
|
||||
token = self.scanner.get_token()
|
||||
end_mark = token.end_mark
|
||||
explicit = True
|
||||
event = DocumentEndEvent(start_mark, end_mark, explicit=explicit)
|
||||
|
||||
# Prepare the next state.
|
||||
if self.resolver.processing_version == (1, 1):
|
||||
self.state = self.parse_document_start
|
||||
else:
|
||||
self.state = self.parse_implicit_document_start
|
||||
|
||||
return event
|
||||
|
||||
def parse_document_content(self):
|
||||
# type: () -> Any
|
||||
if self.scanner.check_token(
|
||||
DirectiveToken, DocumentStartToken, DocumentEndToken, StreamEndToken
|
||||
):
|
||||
event = self.process_empty_scalar(self.scanner.peek_token().start_mark)
|
||||
self.state = self.states.pop()
|
||||
return event
|
||||
else:
|
||||
return self.parse_block_node()
|
||||
|
||||
def process_directives(self):
|
||||
# type: () -> Any
|
||||
yaml_version = None
|
||||
self.tag_handles = {}
|
||||
while self.scanner.check_token(DirectiveToken):
|
||||
token = self.scanner.get_token()
|
||||
if token.name == 'YAML':
|
||||
if yaml_version is not None:
|
||||
raise ParserError(
|
||||
None, None, 'found duplicate YAML directive', token.start_mark
|
||||
)
|
||||
major, minor = token.value
|
||||
if major != 1:
|
||||
raise ParserError(
|
||||
None,
|
||||
None,
|
||||
'found incompatible YAML document (version 1.* is required)',
|
||||
token.start_mark,
|
||||
)
|
||||
yaml_version = token.value
|
||||
elif token.name == 'TAG':
|
||||
handle, prefix = token.value
|
||||
if handle in self.tag_handles:
|
||||
raise ParserError(
|
||||
None,
|
||||
None,
|
||||
_F('duplicate tag handle {handle!r}', handle=handle),
|
||||
token.start_mark,
|
||||
)
|
||||
self.tag_handles[handle] = prefix
|
||||
if bool(self.tag_handles):
|
||||
value = yaml_version, self.tag_handles.copy() # type: Any
|
||||
else:
|
||||
value = yaml_version, None
|
||||
if self.loader is not None and hasattr(self.loader, 'tags'):
|
||||
self.loader.version = yaml_version
|
||||
if self.loader.tags is None:
|
||||
self.loader.tags = {}
|
||||
for k in self.tag_handles:
|
||||
self.loader.tags[k] = self.tag_handles[k]
|
||||
for key in self.DEFAULT_TAGS:
|
||||
if key not in self.tag_handles:
|
||||
self.tag_handles[key] = self.DEFAULT_TAGS[key]
|
||||
return value
|
||||
|
||||
# block_node_or_indentless_sequence ::= ALIAS
|
||||
# | properties (block_content | indentless_block_sequence)?
|
||||
# | block_content
|
||||
# | indentless_block_sequence
|
||||
# block_node ::= ALIAS
|
||||
# | properties block_content?
|
||||
# | block_content
|
||||
# flow_node ::= ALIAS
|
||||
# | properties flow_content?
|
||||
# | flow_content
|
||||
# properties ::= TAG ANCHOR? | ANCHOR TAG?
|
||||
# block_content ::= block_collection | flow_collection | SCALAR
|
||||
# flow_content ::= flow_collection | SCALAR
|
||||
# block_collection ::= block_sequence | block_mapping
|
||||
# flow_collection ::= flow_sequence | flow_mapping
|
||||
|
||||
def parse_block_node(self):
|
||||
# type: () -> Any
|
||||
return self.parse_node(block=True)
|
||||
|
||||
def parse_flow_node(self):
|
||||
# type: () -> Any
|
||||
return self.parse_node()
|
||||
|
||||
def parse_block_node_or_indentless_sequence(self):
|
||||
# type: () -> Any
|
||||
return self.parse_node(block=True, indentless_sequence=True)
|
||||
|
||||
def transform_tag(self, handle, suffix):
|
||||
# type: (Any, Any) -> Any
|
||||
return self.tag_handles[handle] + suffix
|
||||
|
||||
def parse_node(self, block=False, indentless_sequence=False):
|
||||
# type: (bool, bool) -> Any
|
||||
if self.scanner.check_token(AliasToken):
|
||||
token = self.scanner.get_token()
|
||||
event = AliasEvent(token.value, token.start_mark, token.end_mark) # type: Any
|
||||
self.state = self.states.pop()
|
||||
return event
|
||||
|
||||
anchor = None
|
||||
tag = None
|
||||
start_mark = end_mark = tag_mark = None
|
||||
if self.scanner.check_token(AnchorToken):
|
||||
token = self.scanner.get_token()
|
||||
self.move_token_comment(token)
|
||||
start_mark = token.start_mark
|
||||
end_mark = token.end_mark
|
||||
anchor = token.value
|
||||
if self.scanner.check_token(TagToken):
|
||||
token = self.scanner.get_token()
|
||||
tag_mark = token.start_mark
|
||||
end_mark = token.end_mark
|
||||
tag = token.value
|
||||
elif self.scanner.check_token(TagToken):
|
||||
token = self.scanner.get_token()
|
||||
start_mark = tag_mark = token.start_mark
|
||||
end_mark = token.end_mark
|
||||
tag = token.value
|
||||
if self.scanner.check_token(AnchorToken):
|
||||
token = self.scanner.get_token()
|
||||
start_mark = tag_mark = token.start_mark
|
||||
end_mark = token.end_mark
|
||||
anchor = token.value
|
||||
if tag is not None:
|
||||
handle, suffix = tag
|
||||
if handle is not None:
|
||||
if handle not in self.tag_handles:
|
||||
raise ParserError(
|
||||
'while parsing a node',
|
||||
start_mark,
|
||||
_F('found undefined tag handle {handle!r}', handle=handle),
|
||||
tag_mark,
|
||||
)
|
||||
tag = self.transform_tag(handle, suffix)
|
||||
else:
|
||||
tag = suffix
|
||||
# if tag == '!':
|
||||
# raise ParserError("while parsing a node", start_mark,
|
||||
# "found non-specific tag '!'", tag_mark,
|
||||
# "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag'
|
||||
# and share your opinion.")
|
||||
if start_mark is None:
|
||||
start_mark = end_mark = self.scanner.peek_token().start_mark
|
||||
event = None
|
||||
implicit = tag is None or tag == '!'
|
||||
if indentless_sequence and self.scanner.check_token(BlockEntryToken):
|
||||
comment = None
|
||||
pt = self.scanner.peek_token()
|
||||
if self.loader and self.loader.comment_handling is None:
|
||||
if pt.comment and pt.comment[0]:
|
||||
comment = [pt.comment[0], []]
|
||||
pt.comment[0] = None
|
||||
elif self.loader:
|
||||
if pt.comment:
|
||||
comment = pt.comment
|
||||
end_mark = self.scanner.peek_token().end_mark
|
||||
event = SequenceStartEvent(
|
||||
anchor, tag, implicit, start_mark, end_mark, flow_style=False, comment=comment
|
||||
)
|
||||
self.state = self.parse_indentless_sequence_entry
|
||||
return event
|
||||
|
||||
if self.scanner.check_token(ScalarToken):
|
||||
token = self.scanner.get_token()
|
||||
# self.scanner.peek_token_same_line_comment(token)
|
||||
end_mark = token.end_mark
|
||||
if (token.plain and tag is None) or tag == '!':
|
||||
implicit = (True, False)
|
||||
elif tag is None:
|
||||
implicit = (False, True)
|
||||
else:
|
||||
implicit = (False, False)
|
||||
# nprint('se', token.value, token.comment)
|
||||
event = ScalarEvent(
|
||||
anchor,
|
||||
tag,
|
||||
implicit,
|
||||
token.value,
|
||||
start_mark,
|
||||
end_mark,
|
||||
style=token.style,
|
||||
comment=token.comment,
|
||||
)
|
||||
self.state = self.states.pop()
|
||||
elif self.scanner.check_token(FlowSequenceStartToken):
|
||||
pt = self.scanner.peek_token()
|
||||
end_mark = pt.end_mark
|
||||
event = SequenceStartEvent(
|
||||
anchor,
|
||||
tag,
|
||||
implicit,
|
||||
start_mark,
|
||||
end_mark,
|
||||
flow_style=True,
|
||||
comment=pt.comment,
|
||||
)
|
||||
self.state = self.parse_flow_sequence_first_entry
|
||||
elif self.scanner.check_token(FlowMappingStartToken):
|
||||
pt = self.scanner.peek_token()
|
||||
end_mark = pt.end_mark
|
||||
event = MappingStartEvent(
|
||||
anchor,
|
||||
tag,
|
||||
implicit,
|
||||
start_mark,
|
||||
end_mark,
|
||||
flow_style=True,
|
||||
comment=pt.comment,
|
||||
)
|
||||
self.state = self.parse_flow_mapping_first_key
|
||||
elif block and self.scanner.check_token(BlockSequenceStartToken):
|
||||
end_mark = self.scanner.peek_token().start_mark
|
||||
# should inserting the comment be dependent on the
|
||||
# indentation?
|
||||
pt = self.scanner.peek_token()
|
||||
comment = pt.comment
|
||||
# nprint('pt0', type(pt))
|
||||
if comment is None or comment[1] is None:
|
||||
comment = pt.split_old_comment()
|
||||
# nprint('pt1', comment)
|
||||
event = SequenceStartEvent(
|
||||
anchor, tag, implicit, start_mark, end_mark, flow_style=False, comment=comment
|
||||
)
|
||||
self.state = self.parse_block_sequence_first_entry
|
||||
elif block and self.scanner.check_token(BlockMappingStartToken):
|
||||
end_mark = self.scanner.peek_token().start_mark
|
||||
comment = self.scanner.peek_token().comment
|
||||
event = MappingStartEvent(
|
||||
anchor, tag, implicit, start_mark, end_mark, flow_style=False, comment=comment
|
||||
)
|
||||
self.state = self.parse_block_mapping_first_key
|
||||
elif anchor is not None or tag is not None:
|
||||
# Empty scalars are allowed even if a tag or an anchor is
|
||||
# specified.
|
||||
event = ScalarEvent(anchor, tag, (implicit, False), "", start_mark, end_mark)
|
||||
self.state = self.states.pop()
|
||||
else:
|
||||
if block:
|
||||
node = 'block'
|
||||
else:
|
||||
node = 'flow'
|
||||
token = self.scanner.peek_token()
|
||||
raise ParserError(
|
||||
_F('while parsing a {node!s} node', node=node),
|
||||
start_mark,
|
||||
_F('expected the node content, but found {token_id!r}', token_id=token.id),
|
||||
token.start_mark,
|
||||
)
|
||||
return event
|
||||
|
||||
# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)*
|
||||
# BLOCK-END
|
||||
|
||||
def parse_block_sequence_first_entry(self):
|
||||
# type: () -> Any
|
||||
token = self.scanner.get_token()
|
||||
# move any comment from start token
|
||||
# self.move_token_comment(token)
|
||||
self.marks.append(token.start_mark)
|
||||
return self.parse_block_sequence_entry()
|
||||
|
||||
def parse_block_sequence_entry(self):
|
||||
# type: () -> Any
|
||||
if self.scanner.check_token(BlockEntryToken):
|
||||
token = self.scanner.get_token()
|
||||
self.move_token_comment(token)
|
||||
if not self.scanner.check_token(BlockEntryToken, BlockEndToken):
|
||||
self.states.append(self.parse_block_sequence_entry)
|
||||
return self.parse_block_node()
|
||||
else:
|
||||
self.state = self.parse_block_sequence_entry
|
||||
return self.process_empty_scalar(token.end_mark)
|
||||
if not self.scanner.check_token(BlockEndToken):
|
||||
token = self.scanner.peek_token()
|
||||
raise ParserError(
|
||||
'while parsing a block collection',
|
||||
self.marks[-1],
|
||||
_F('expected <block end>, but found {token_id!r}', token_id=token.id),
|
||||
token.start_mark,
|
||||
)
|
||||
token = self.scanner.get_token() # BlockEndToken
|
||||
event = SequenceEndEvent(token.start_mark, token.end_mark, comment=token.comment)
|
||||
self.state = self.states.pop()
|
||||
self.marks.pop()
|
||||
return event
|
||||
|
||||
# indentless_sequence ::= (BLOCK-ENTRY block_node?)+
|
||||
|
||||
# indentless_sequence?
|
||||
# sequence:
|
||||
# - entry
|
||||
# - nested
|
||||
|
||||
def parse_indentless_sequence_entry(self):
|
||||
# type: () -> Any
|
||||
if self.scanner.check_token(BlockEntryToken):
|
||||
token = self.scanner.get_token()
|
||||
self.move_token_comment(token)
|
||||
if not self.scanner.check_token(
|
||||
BlockEntryToken, KeyToken, ValueToken, BlockEndToken
|
||||
):
|
||||
self.states.append(self.parse_indentless_sequence_entry)
|
||||
return self.parse_block_node()
|
||||
else:
|
||||
self.state = self.parse_indentless_sequence_entry
|
||||
return self.process_empty_scalar(token.end_mark)
|
||||
token = self.scanner.peek_token()
|
||||
c = None
|
||||
if self.loader and self.loader.comment_handling is None:
|
||||
c = token.comment
|
||||
start_mark = token.start_mark
|
||||
else:
|
||||
start_mark = self.last_event.end_mark # type: ignore
|
||||
c = self.distribute_comment(token.comment, start_mark.line) # type: ignore
|
||||
event = SequenceEndEvent(start_mark, start_mark, comment=c)
|
||||
self.state = self.states.pop()
|
||||
return event
|
||||
|
||||
# block_mapping ::= BLOCK-MAPPING_START
|
||||
# ((KEY block_node_or_indentless_sequence?)?
|
||||
# (VALUE block_node_or_indentless_sequence?)?)*
|
||||
# BLOCK-END
|
||||
|
||||
def parse_block_mapping_first_key(self):
|
||||
# type: () -> Any
|
||||
token = self.scanner.get_token()
|
||||
self.marks.append(token.start_mark)
|
||||
return self.parse_block_mapping_key()
|
||||
|
||||
def parse_block_mapping_key(self):
|
||||
# type: () -> Any
|
||||
if self.scanner.check_token(KeyToken):
|
||||
token = self.scanner.get_token()
|
||||
self.move_token_comment(token)
|
||||
if not self.scanner.check_token(KeyToken, ValueToken, BlockEndToken):
|
||||
self.states.append(self.parse_block_mapping_value)
|
||||
return self.parse_block_node_or_indentless_sequence()
|
||||
else:
|
||||
self.state = self.parse_block_mapping_value
|
||||
return self.process_empty_scalar(token.end_mark)
|
||||
if self.resolver.processing_version > (1, 1) and self.scanner.check_token(ValueToken):
|
||||
self.state = self.parse_block_mapping_value
|
||||
return self.process_empty_scalar(self.scanner.peek_token().start_mark)
|
||||
if not self.scanner.check_token(BlockEndToken):
|
||||
token = self.scanner.peek_token()
|
||||
raise ParserError(
|
||||
'while parsing a block mapping',
|
||||
self.marks[-1],
|
||||
_F('expected <block end>, but found {token_id!r}', token_id=token.id),
|
||||
token.start_mark,
|
||||
)
|
||||
token = self.scanner.get_token()
|
||||
self.move_token_comment(token)
|
||||
event = MappingEndEvent(token.start_mark, token.end_mark, comment=token.comment)
|
||||
self.state = self.states.pop()
|
||||
self.marks.pop()
|
||||
return event
|
||||
|
||||
def parse_block_mapping_value(self):
|
||||
# type: () -> Any
|
||||
if self.scanner.check_token(ValueToken):
|
||||
token = self.scanner.get_token()
|
||||
# value token might have post comment move it to e.g. block
|
||||
if self.scanner.check_token(ValueToken):
|
||||
self.move_token_comment(token)
|
||||
else:
|
||||
if not self.scanner.check_token(KeyToken):
|
||||
self.move_token_comment(token, empty=True)
|
||||
# else: empty value for this key cannot move token.comment
|
||||
if not self.scanner.check_token(KeyToken, ValueToken, BlockEndToken):
|
||||
self.states.append(self.parse_block_mapping_key)
|
||||
return self.parse_block_node_or_indentless_sequence()
|
||||
else:
|
||||
self.state = self.parse_block_mapping_key
|
||||
comment = token.comment
|
||||
if comment is None:
|
||||
token = self.scanner.peek_token()
|
||||
comment = token.comment
|
||||
if comment:
|
||||
token._comment = [None, comment[1]]
|
||||
comment = [comment[0], None]
|
||||
return self.process_empty_scalar(token.end_mark, comment=comment)
|
||||
else:
|
||||
self.state = self.parse_block_mapping_key
|
||||
token = self.scanner.peek_token()
|
||||
return self.process_empty_scalar(token.start_mark)
|
||||
|
||||
# flow_sequence ::= FLOW-SEQUENCE-START
|
||||
# (flow_sequence_entry FLOW-ENTRY)*
|
||||
# flow_sequence_entry?
|
||||
# FLOW-SEQUENCE-END
|
||||
# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
#
|
||||
# Note that while production rules for both flow_sequence_entry and
|
||||
# flow_mapping_entry are equal, their interpretations are different.
|
||||
# For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?`
|
||||
# generate an inline mapping (set syntax).
|
||||
|
||||
def parse_flow_sequence_first_entry(self):
|
||||
# type: () -> Any
|
||||
token = self.scanner.get_token()
|
||||
self.marks.append(token.start_mark)
|
||||
return self.parse_flow_sequence_entry(first=True)
|
||||
|
||||
def parse_flow_sequence_entry(self, first=False):
|
||||
# type: (bool) -> Any
|
||||
if not self.scanner.check_token(FlowSequenceEndToken):
|
||||
if not first:
|
||||
if self.scanner.check_token(FlowEntryToken):
|
||||
self.scanner.get_token()
|
||||
else:
|
||||
token = self.scanner.peek_token()
|
||||
raise ParserError(
|
||||
'while parsing a flow sequence',
|
||||
self.marks[-1],
|
||||
_F("expected ',' or ']', but got {token_id!r}", token_id=token.id),
|
||||
token.start_mark,
|
||||
)
|
||||
|
||||
if self.scanner.check_token(KeyToken):
|
||||
token = self.scanner.peek_token()
|
||||
event = MappingStartEvent(
|
||||
None, None, True, token.start_mark, token.end_mark, flow_style=True
|
||||
) # type: Any
|
||||
self.state = self.parse_flow_sequence_entry_mapping_key
|
||||
return event
|
||||
elif not self.scanner.check_token(FlowSequenceEndToken):
|
||||
self.states.append(self.parse_flow_sequence_entry)
|
||||
return self.parse_flow_node()
|
||||
token = self.scanner.get_token()
|
||||
event = SequenceEndEvent(token.start_mark, token.end_mark, comment=token.comment)
|
||||
self.state = self.states.pop()
|
||||
self.marks.pop()
|
||||
return event
|
||||
|
||||
def parse_flow_sequence_entry_mapping_key(self):
|
||||
# type: () -> Any
|
||||
token = self.scanner.get_token()
|
||||
if not self.scanner.check_token(ValueToken, FlowEntryToken, FlowSequenceEndToken):
|
||||
self.states.append(self.parse_flow_sequence_entry_mapping_value)
|
||||
return self.parse_flow_node()
|
||||
else:
|
||||
self.state = self.parse_flow_sequence_entry_mapping_value
|
||||
return self.process_empty_scalar(token.end_mark)
|
||||
|
||||
def parse_flow_sequence_entry_mapping_value(self):
|
||||
# type: () -> Any
|
||||
if self.scanner.check_token(ValueToken):
|
||||
token = self.scanner.get_token()
|
||||
if not self.scanner.check_token(FlowEntryToken, FlowSequenceEndToken):
|
||||
self.states.append(self.parse_flow_sequence_entry_mapping_end)
|
||||
return self.parse_flow_node()
|
||||
else:
|
||||
self.state = self.parse_flow_sequence_entry_mapping_end
|
||||
return self.process_empty_scalar(token.end_mark)
|
||||
else:
|
||||
self.state = self.parse_flow_sequence_entry_mapping_end
|
||||
token = self.scanner.peek_token()
|
||||
return self.process_empty_scalar(token.start_mark)
|
||||
|
||||
def parse_flow_sequence_entry_mapping_end(self):
|
||||
# type: () -> Any
|
||||
self.state = self.parse_flow_sequence_entry
|
||||
token = self.scanner.peek_token()
|
||||
return MappingEndEvent(token.start_mark, token.start_mark)
|
||||
|
||||
# flow_mapping ::= FLOW-MAPPING-START
|
||||
# (flow_mapping_entry FLOW-ENTRY)*
|
||||
# flow_mapping_entry?
|
||||
# FLOW-MAPPING-END
|
||||
# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
|
||||
def parse_flow_mapping_first_key(self):
|
||||
# type: () -> Any
|
||||
token = self.scanner.get_token()
|
||||
self.marks.append(token.start_mark)
|
||||
return self.parse_flow_mapping_key(first=True)
|
||||
|
||||
def parse_flow_mapping_key(self, first=False):
|
||||
# type: (Any) -> Any
|
||||
if not self.scanner.check_token(FlowMappingEndToken):
|
||||
if not first:
|
||||
if self.scanner.check_token(FlowEntryToken):
|
||||
self.scanner.get_token()
|
||||
else:
|
||||
token = self.scanner.peek_token()
|
||||
raise ParserError(
|
||||
'while parsing a flow mapping',
|
||||
self.marks[-1],
|
||||
_F("expected ',' or '}}', but got {token_id!r}", token_id=token.id),
|
||||
token.start_mark,
|
||||
)
|
||||
if self.scanner.check_token(KeyToken):
|
||||
token = self.scanner.get_token()
|
||||
if not self.scanner.check_token(
|
||||
ValueToken, FlowEntryToken, FlowMappingEndToken
|
||||
):
|
||||
self.states.append(self.parse_flow_mapping_value)
|
||||
return self.parse_flow_node()
|
||||
else:
|
||||
self.state = self.parse_flow_mapping_value
|
||||
return self.process_empty_scalar(token.end_mark)
|
||||
elif self.resolver.processing_version > (1, 1) and self.scanner.check_token(
|
||||
ValueToken
|
||||
):
|
||||
self.state = self.parse_flow_mapping_value
|
||||
return self.process_empty_scalar(self.scanner.peek_token().end_mark)
|
||||
elif not self.scanner.check_token(FlowMappingEndToken):
|
||||
self.states.append(self.parse_flow_mapping_empty_value)
|
||||
return self.parse_flow_node()
|
||||
token = self.scanner.get_token()
|
||||
event = MappingEndEvent(token.start_mark, token.end_mark, comment=token.comment)
|
||||
self.state = self.states.pop()
|
||||
self.marks.pop()
|
||||
return event
|
||||
|
||||
def parse_flow_mapping_value(self):
|
||||
# type: () -> Any
|
||||
if self.scanner.check_token(ValueToken):
|
||||
token = self.scanner.get_token()
|
||||
if not self.scanner.check_token(FlowEntryToken, FlowMappingEndToken):
|
||||
self.states.append(self.parse_flow_mapping_key)
|
||||
return self.parse_flow_node()
|
||||
else:
|
||||
self.state = self.parse_flow_mapping_key
|
||||
return self.process_empty_scalar(token.end_mark)
|
||||
else:
|
||||
self.state = self.parse_flow_mapping_key
|
||||
token = self.scanner.peek_token()
|
||||
return self.process_empty_scalar(token.start_mark)
|
||||
|
||||
def parse_flow_mapping_empty_value(self):
|
||||
# type: () -> Any
|
||||
self.state = self.parse_flow_mapping_key
|
||||
return self.process_empty_scalar(self.scanner.peek_token().start_mark)
|
||||
|
||||
def process_empty_scalar(self, mark, comment=None):
|
||||
# type: (Any, Any) -> Any
|
||||
return ScalarEvent(None, None, (True, False), "", mark, mark, comment=comment)
|
||||
|
||||
def move_token_comment(self, token, nt=None, empty=False):
|
||||
# type: (Any, Optional[Any], Optional[bool]) -> Any
|
||||
pass
|
||||
|
||||
|
||||
class RoundTripParser(Parser):
|
||||
"""roundtrip is a safe loader, that wants to see the unmangled tag"""
|
||||
|
||||
def transform_tag(self, handle, suffix):
|
||||
# type: (Any, Any) -> Any
|
||||
# return self.tag_handles[handle]+suffix
|
||||
if handle == '!!' and suffix in (
|
||||
'null',
|
||||
'bool',
|
||||
'int',
|
||||
'float',
|
||||
'binary',
|
||||
'timestamp',
|
||||
'omap',
|
||||
'pairs',
|
||||
'set',
|
||||
'str',
|
||||
'seq',
|
||||
'map',
|
||||
):
|
||||
return Parser.transform_tag(self, handle, suffix)
|
||||
return handle + suffix
|
||||
|
||||
def move_token_comment(self, token, nt=None, empty=False):
|
||||
# type: (Any, Optional[Any], Optional[bool]) -> Any
|
||||
token.move_old_comment(self.scanner.peek_token() if nt is None else nt, empty=empty)
|
||||
|
||||
|
||||
class RoundTripParserSC(RoundTripParser):
|
||||
"""roundtrip is a safe loader, that wants to see the unmangled tag"""
|
||||
|
||||
# some of the differences are based on the superclass testing
|
||||
# if self.loader.comment_handling is not None
|
||||
|
||||
def move_token_comment(self, token, nt=None, empty=False):
|
||||
# type: (Any, Any, Any, Optional[bool]) -> None
|
||||
token.move_new_comment(self.scanner.peek_token() if nt is None else nt, empty=empty)
|
||||
|
||||
def distribute_comment(self, comment, line):
|
||||
# type: (Any, Any) -> Any
|
||||
# ToDo, look at indentation of the comment to determine attachment
|
||||
if comment is None:
|
||||
return None
|
||||
if not comment[0]:
|
||||
return None
|
||||
if comment[0][0] != line + 1:
|
||||
nprintf('>>>dcxxx', comment, line)
|
||||
assert comment[0][0] == line + 1
|
||||
# if comment[0] - line > 1:
|
||||
# return
|
||||
typ = self.loader.comment_handling & 0b11
|
||||
# nprintf('>>>dca', comment, line, typ)
|
||||
if typ == C_POST:
|
||||
return None
|
||||
if typ == C_PRE:
|
||||
c = [None, None, comment[0]]
|
||||
comment[0] = None
|
||||
return c
|
||||
# nprintf('>>>dcb', comment[0])
|
||||
for _idx, cmntidx in enumerate(comment[0]):
|
||||
# nprintf('>>>dcb', cmntidx)
|
||||
if isinstance(self.scanner.comments[cmntidx], BlankLineComment):
|
||||
break
|
||||
else:
|
||||
return None # no space found
|
||||
if _idx == 0:
|
||||
return None # first line was blank
|
||||
# nprintf('>>>dcc', idx)
|
||||
if typ == C_SPLIT_ON_FIRST_BLANK:
|
||||
c = [None, None, comment[0][:_idx]]
|
||||
comment[0] = comment[0][_idx:]
|
||||
return c
|
||||
raise NotImplementedError # reserved
|
||||
Vendored
+302
@@ -0,0 +1,302 @@
|
||||
# coding: utf-8
|
||||
|
||||
# This module contains abstractions for the input stream. You don't have to
|
||||
# looks further, there are no pretty code.
|
||||
#
|
||||
# We define two classes here.
|
||||
#
|
||||
# Mark(source, line, column)
|
||||
# It's just a record and its only use is producing nice error messages.
|
||||
# Parser does not use it for any other purposes.
|
||||
#
|
||||
# Reader(source, data)
|
||||
# Reader determines the encoding of `data` and converts it to unicode.
|
||||
# Reader provides the following methods and attributes:
|
||||
# reader.peek(length=1) - return the next `length` characters
|
||||
# reader.forward(length=1) - move the current position to `length`
|
||||
# characters.
|
||||
# reader.index - the number of the current character.
|
||||
# reader.line, stream.column - the line and the column of the current
|
||||
# character.
|
||||
|
||||
import codecs
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.error import YAMLError, FileMark, StringMark, YAMLStreamError
|
||||
from pipenv.vendor.ruamel.yaml.compat import _F # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.util import RegExp
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List, Union, Text, Tuple, Optional # NOQA
|
||||
# from ruamel.compat import StreamTextType # NOQA
|
||||
|
||||
__all__ = ['Reader', 'ReaderError']
|
||||
|
||||
|
||||
class ReaderError(YAMLError):
|
||||
def __init__(self, name, position, character, encoding, reason):
|
||||
# type: (Any, Any, Any, Any, Any) -> None
|
||||
self.name = name
|
||||
self.character = character
|
||||
self.position = position
|
||||
self.encoding = encoding
|
||||
self.reason = reason
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> Any
|
||||
if isinstance(self.character, bytes):
|
||||
return _F(
|
||||
"'{self_encoding!s}' codec can't decode byte #x{ord_self_character:02x}: "
|
||||
'{self_reason!s}\n'
|
||||
' in "{self_name!s}", position {self_position:d}',
|
||||
self_encoding=self.encoding,
|
||||
ord_self_character=ord(self.character),
|
||||
self_reason=self.reason,
|
||||
self_name=self.name,
|
||||
self_position=self.position,
|
||||
)
|
||||
else:
|
||||
return _F(
|
||||
'unacceptable character #x{self_character:04x}: {self_reason!s}\n'
|
||||
' in "{self_name!s}", position {self_position:d}',
|
||||
self_character=self.character,
|
||||
self_reason=self.reason,
|
||||
self_name=self.name,
|
||||
self_position=self.position,
|
||||
)
|
||||
|
||||
|
||||
class Reader:
|
||||
# Reader:
|
||||
# - determines the data encoding and converts it to a unicode string,
|
||||
# - checks if characters are in allowed range,
|
||||
# - adds '\0' to the end.
|
||||
|
||||
# Reader accepts
|
||||
# - a `bytes` object,
|
||||
# - a `str` object,
|
||||
# - a file-like object with its `read` method returning `str`,
|
||||
# - a file-like object with its `read` method returning `unicode`.
|
||||
|
||||
# Yeah, it's ugly and slow.
|
||||
|
||||
def __init__(self, stream, loader=None):
|
||||
# type: (Any, Any) -> None
|
||||
self.loader = loader
|
||||
if self.loader is not None and getattr(self.loader, '_reader', None) is None:
|
||||
self.loader._reader = self
|
||||
self.reset_reader()
|
||||
self.stream = stream # type: Any # as .read is called
|
||||
|
||||
def reset_reader(self):
|
||||
# type: () -> None
|
||||
self.name = None # type: Any
|
||||
self.stream_pointer = 0
|
||||
self.eof = True
|
||||
self.buffer = ""
|
||||
self.pointer = 0
|
||||
self.raw_buffer = None # type: Any
|
||||
self.raw_decode = None
|
||||
self.encoding = None # type: Optional[Text]
|
||||
self.index = 0
|
||||
self.line = 0
|
||||
self.column = 0
|
||||
|
||||
@property
|
||||
def stream(self):
|
||||
# type: () -> Any
|
||||
try:
|
||||
return self._stream
|
||||
except AttributeError:
|
||||
raise YAMLStreamError('input stream needs to specified')
|
||||
|
||||
@stream.setter
|
||||
def stream(self, val):
|
||||
# type: (Any) -> None
|
||||
if val is None:
|
||||
return
|
||||
self._stream = None
|
||||
if isinstance(val, str):
|
||||
self.name = '<unicode string>'
|
||||
self.check_printable(val)
|
||||
self.buffer = val + '\0'
|
||||
elif isinstance(val, bytes):
|
||||
self.name = '<byte string>'
|
||||
self.raw_buffer = val
|
||||
self.determine_encoding()
|
||||
else:
|
||||
if not hasattr(val, 'read'):
|
||||
raise YAMLStreamError('stream argument needs to have a read() method')
|
||||
self._stream = val
|
||||
self.name = getattr(self.stream, 'name', '<file>')
|
||||
self.eof = False
|
||||
self.raw_buffer = None
|
||||
self.determine_encoding()
|
||||
|
||||
def peek(self, index=0):
|
||||
# type: (int) -> Text
|
||||
try:
|
||||
return self.buffer[self.pointer + index]
|
||||
except IndexError:
|
||||
self.update(index + 1)
|
||||
return self.buffer[self.pointer + index]
|
||||
|
||||
def prefix(self, length=1):
|
||||
# type: (int) -> Any
|
||||
if self.pointer + length >= len(self.buffer):
|
||||
self.update(length)
|
||||
return self.buffer[self.pointer : self.pointer + length]
|
||||
|
||||
def forward_1_1(self, length=1):
|
||||
# type: (int) -> None
|
||||
if self.pointer + length + 1 >= len(self.buffer):
|
||||
self.update(length + 1)
|
||||
while length != 0:
|
||||
ch = self.buffer[self.pointer]
|
||||
self.pointer += 1
|
||||
self.index += 1
|
||||
if ch in '\n\x85\u2028\u2029' or (
|
||||
ch == '\r' and self.buffer[self.pointer] != '\n'
|
||||
):
|
||||
self.line += 1
|
||||
self.column = 0
|
||||
elif ch != '\uFEFF':
|
||||
self.column += 1
|
||||
length -= 1
|
||||
|
||||
def forward(self, length=1):
|
||||
# type: (int) -> None
|
||||
if self.pointer + length + 1 >= len(self.buffer):
|
||||
self.update(length + 1)
|
||||
while length != 0:
|
||||
ch = self.buffer[self.pointer]
|
||||
self.pointer += 1
|
||||
self.index += 1
|
||||
if ch == '\n' or (ch == '\r' and self.buffer[self.pointer] != '\n'):
|
||||
self.line += 1
|
||||
self.column = 0
|
||||
elif ch != '\uFEFF':
|
||||
self.column += 1
|
||||
length -= 1
|
||||
|
||||
def get_mark(self):
|
||||
# type: () -> Any
|
||||
if self.stream is None:
|
||||
return StringMark(
|
||||
self.name, self.index, self.line, self.column, self.buffer, self.pointer
|
||||
)
|
||||
else:
|
||||
return FileMark(self.name, self.index, self.line, self.column)
|
||||
|
||||
def determine_encoding(self):
|
||||
# type: () -> None
|
||||
while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2):
|
||||
self.update_raw()
|
||||
if isinstance(self.raw_buffer, bytes):
|
||||
if self.raw_buffer.startswith(codecs.BOM_UTF16_LE):
|
||||
self.raw_decode = codecs.utf_16_le_decode # type: ignore
|
||||
self.encoding = 'utf-16-le'
|
||||
elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE):
|
||||
self.raw_decode = codecs.utf_16_be_decode # type: ignore
|
||||
self.encoding = 'utf-16-be'
|
||||
else:
|
||||
self.raw_decode = codecs.utf_8_decode # type: ignore
|
||||
self.encoding = 'utf-8'
|
||||
self.update(1)
|
||||
|
||||
NON_PRINTABLE = RegExp(
|
||||
'[^\x09\x0A\x0D\x20-\x7E\x85' '\xA0-\uD7FF' '\uE000-\uFFFD' '\U00010000-\U0010FFFF' ']'
|
||||
)
|
||||
|
||||
_printable_ascii = ('\x09\x0A\x0D' + "".join(map(chr, range(0x20, 0x7F)))).encode('ascii')
|
||||
|
||||
@classmethod
|
||||
def _get_non_printable_ascii(cls, data): # type: ignore
|
||||
# type: (Text, bytes) -> Optional[Tuple[int, Text]]
|
||||
ascii_bytes = data.encode('ascii') # type: ignore
|
||||
non_printables = ascii_bytes.translate(None, cls._printable_ascii) # type: ignore
|
||||
if not non_printables:
|
||||
return None
|
||||
non_printable = non_printables[:1]
|
||||
return ascii_bytes.index(non_printable), non_printable.decode('ascii')
|
||||
|
||||
@classmethod
|
||||
def _get_non_printable_regex(cls, data):
|
||||
# type: (Text) -> Optional[Tuple[int, Text]]
|
||||
match = cls.NON_PRINTABLE.search(data)
|
||||
if not bool(match):
|
||||
return None
|
||||
return match.start(), match.group()
|
||||
|
||||
@classmethod
|
||||
def _get_non_printable(cls, data):
|
||||
# type: (Text) -> Optional[Tuple[int, Text]]
|
||||
try:
|
||||
return cls._get_non_printable_ascii(data) # type: ignore
|
||||
except UnicodeEncodeError:
|
||||
return cls._get_non_printable_regex(data)
|
||||
|
||||
def check_printable(self, data):
|
||||
# type: (Any) -> None
|
||||
non_printable_match = self._get_non_printable(data)
|
||||
if non_printable_match is not None:
|
||||
start, character = non_printable_match
|
||||
position = self.index + (len(self.buffer) - self.pointer) + start
|
||||
raise ReaderError(
|
||||
self.name,
|
||||
position,
|
||||
ord(character),
|
||||
'unicode',
|
||||
'special characters are not allowed',
|
||||
)
|
||||
|
||||
def update(self, length):
|
||||
# type: (int) -> None
|
||||
if self.raw_buffer is None:
|
||||
return
|
||||
self.buffer = self.buffer[self.pointer :]
|
||||
self.pointer = 0
|
||||
while len(self.buffer) < length:
|
||||
if not self.eof:
|
||||
self.update_raw()
|
||||
if self.raw_decode is not None:
|
||||
try:
|
||||
data, converted = self.raw_decode(self.raw_buffer, 'strict', self.eof)
|
||||
except UnicodeDecodeError as exc:
|
||||
character = self.raw_buffer[exc.start]
|
||||
if self.stream is not None:
|
||||
position = self.stream_pointer - len(self.raw_buffer) + exc.start
|
||||
elif self.stream is not None:
|
||||
position = self.stream_pointer - len(self.raw_buffer) + exc.start
|
||||
else:
|
||||
position = exc.start
|
||||
raise ReaderError(self.name, position, character, exc.encoding, exc.reason)
|
||||
else:
|
||||
data = self.raw_buffer
|
||||
converted = len(data)
|
||||
self.check_printable(data)
|
||||
self.buffer += data
|
||||
self.raw_buffer = self.raw_buffer[converted:]
|
||||
if self.eof:
|
||||
self.buffer += '\0'
|
||||
self.raw_buffer = None
|
||||
break
|
||||
|
||||
def update_raw(self, size=None):
|
||||
# type: (Optional[int]) -> None
|
||||
if size is None:
|
||||
size = 4096
|
||||
data = self.stream.read(size)
|
||||
if self.raw_buffer is None:
|
||||
self.raw_buffer = data
|
||||
else:
|
||||
self.raw_buffer += data
|
||||
self.stream_pointer += len(data)
|
||||
if not data:
|
||||
self.eof = True
|
||||
|
||||
|
||||
# try:
|
||||
# import psyco
|
||||
# psyco.bind(Reader)
|
||||
# except ImportError:
|
||||
# pass
|
||||
+1156
File diff suppressed because it is too large
Load Diff
+405
@@ -0,0 +1,405 @@
|
||||
# coding: utf-8
|
||||
|
||||
import re
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, List, Union, Text, Optional # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.compat import VersionType # NOQA
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.compat import _DEFAULT_YAML_VERSION, _F # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.error import * # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.nodes import MappingNode, ScalarNode, SequenceNode # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.util import RegExp # NOQA
|
||||
|
||||
__all__ = ['BaseResolver', 'Resolver', 'VersionedResolver']
|
||||
|
||||
|
||||
# fmt: off
|
||||
# resolvers consist of
|
||||
# - a list of applicable version
|
||||
# - a tag
|
||||
# - a regexp
|
||||
# - a list of first characters to match
|
||||
implicit_resolvers = [
|
||||
([(1, 2)],
|
||||
'tag:yaml.org,2002:bool',
|
||||
RegExp('''^(?:true|True|TRUE|false|False|FALSE)$''', re.X),
|
||||
list('tTfF')),
|
||||
([(1, 1)],
|
||||
'tag:yaml.org,2002:bool',
|
||||
RegExp('''^(?:y|Y|yes|Yes|YES|n|N|no|No|NO
|
||||
|true|True|TRUE|false|False|FALSE
|
||||
|on|On|ON|off|Off|OFF)$''', re.X),
|
||||
list('yYnNtTfFoO')),
|
||||
([(1, 2)],
|
||||
'tag:yaml.org,2002:float',
|
||||
RegExp('''^(?:
|
||||
[-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?
|
||||
|[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
|
||||
|[-+]?\\.[0-9_]+(?:[eE][-+][0-9]+)?
|
||||
|[-+]?\\.(?:inf|Inf|INF)
|
||||
|\\.(?:nan|NaN|NAN))$''', re.X),
|
||||
list('-+0123456789.')),
|
||||
([(1, 1)],
|
||||
'tag:yaml.org,2002:float',
|
||||
RegExp('''^(?:
|
||||
[-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?
|
||||
|[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
|
||||
|\\.[0-9_]+(?:[eE][-+][0-9]+)?
|
||||
|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]* # sexagesimal float
|
||||
|[-+]?\\.(?:inf|Inf|INF)
|
||||
|\\.(?:nan|NaN|NAN))$''', re.X),
|
||||
list('-+0123456789.')),
|
||||
([(1, 2)],
|
||||
'tag:yaml.org,2002:int',
|
||||
RegExp('''^(?:[-+]?0b[0-1_]+
|
||||
|[-+]?0o?[0-7_]+
|
||||
|[-+]?[0-9_]+
|
||||
|[-+]?0x[0-9a-fA-F_]+)$''', re.X),
|
||||
list('-+0123456789')),
|
||||
([(1, 1)],
|
||||
'tag:yaml.org,2002:int',
|
||||
RegExp('''^(?:[-+]?0b[0-1_]+
|
||||
|[-+]?0?[0-7_]+
|
||||
|[-+]?(?:0|[1-9][0-9_]*)
|
||||
|[-+]?0x[0-9a-fA-F_]+
|
||||
|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), # sexagesimal int
|
||||
list('-+0123456789')),
|
||||
([(1, 2), (1, 1)],
|
||||
'tag:yaml.org,2002:merge',
|
||||
RegExp('^(?:<<)$'),
|
||||
['<']),
|
||||
([(1, 2), (1, 1)],
|
||||
'tag:yaml.org,2002:null',
|
||||
RegExp('''^(?: ~
|
||||
|null|Null|NULL
|
||||
| )$''', re.X),
|
||||
['~', 'n', 'N', '']),
|
||||
([(1, 2), (1, 1)],
|
||||
'tag:yaml.org,2002:timestamp',
|
||||
RegExp('''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
|
||||
|[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]?
|
||||
(?:[Tt]|[ \\t]+)[0-9][0-9]?
|
||||
:[0-9][0-9] :[0-9][0-9] (?:\\.[0-9]*)?
|
||||
(?:[ \\t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X),
|
||||
list('0123456789')),
|
||||
([(1, 2), (1, 1)],
|
||||
'tag:yaml.org,2002:value',
|
||||
RegExp('^(?:=)$'),
|
||||
['=']),
|
||||
# The following resolver is only for documentation purposes. It cannot work
|
||||
# because plain scalars cannot start with '!', '&', or '*'.
|
||||
([(1, 2), (1, 1)],
|
||||
'tag:yaml.org,2002:yaml',
|
||||
RegExp('^(?:!|&|\\*)$'),
|
||||
list('!&*')),
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
class ResolverError(YAMLError):
|
||||
pass
|
||||
|
||||
|
||||
class BaseResolver:
|
||||
|
||||
DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str'
|
||||
DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq'
|
||||
DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map'
|
||||
|
||||
yaml_implicit_resolvers = {} # type: Dict[Any, Any]
|
||||
yaml_path_resolvers = {} # type: Dict[Any, Any]
|
||||
|
||||
def __init__(self, loadumper=None):
|
||||
# type: (Any, Any) -> None
|
||||
self.loadumper = loadumper
|
||||
if self.loadumper is not None and getattr(self.loadumper, '_resolver', None) is None:
|
||||
self.loadumper._resolver = self.loadumper
|
||||
self._loader_version = None # type: Any
|
||||
self.resolver_exact_paths = [] # type: List[Any]
|
||||
self.resolver_prefix_paths = [] # type: List[Any]
|
||||
|
||||
@property
|
||||
def parser(self):
|
||||
# type: () -> Any
|
||||
if self.loadumper is not None:
|
||||
if hasattr(self.loadumper, 'typ'):
|
||||
return self.loadumper.parser
|
||||
return self.loadumper._parser
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def add_implicit_resolver_base(cls, tag, regexp, first):
|
||||
# type: (Any, Any, Any) -> None
|
||||
if 'yaml_implicit_resolvers' not in cls.__dict__:
|
||||
# deepcopy doesn't work here
|
||||
cls.yaml_implicit_resolvers = dict(
|
||||
(k, cls.yaml_implicit_resolvers[k][:]) for k in cls.yaml_implicit_resolvers
|
||||
)
|
||||
if first is None:
|
||||
first = [None]
|
||||
for ch in first:
|
||||
cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp))
|
||||
|
||||
@classmethod
|
||||
def add_implicit_resolver(cls, tag, regexp, first):
|
||||
# type: (Any, Any, Any) -> None
|
||||
if 'yaml_implicit_resolvers' not in cls.__dict__:
|
||||
# deepcopy doesn't work here
|
||||
cls.yaml_implicit_resolvers = dict(
|
||||
(k, cls.yaml_implicit_resolvers[k][:]) for k in cls.yaml_implicit_resolvers
|
||||
)
|
||||
if first is None:
|
||||
first = [None]
|
||||
for ch in first:
|
||||
cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp))
|
||||
implicit_resolvers.append(([(1, 2), (1, 1)], tag, regexp, first))
|
||||
|
||||
# @classmethod
|
||||
# def add_implicit_resolver(cls, tag, regexp, first):
|
||||
|
||||
@classmethod
|
||||
def add_path_resolver(cls, tag, path, kind=None):
|
||||
# type: (Any, Any, Any) -> None
|
||||
# Note: `add_path_resolver` is experimental. The API could be changed.
|
||||
# `new_path` is a pattern that is matched against the path from the
|
||||
# root to the node that is being considered. `node_path` elements are
|
||||
# tuples `(node_check, index_check)`. `node_check` is a node class:
|
||||
# `ScalarNode`, `SequenceNode`, `MappingNode` or `None`. `None`
|
||||
# matches any kind of a node. `index_check` could be `None`, a boolean
|
||||
# value, a string value, or a number. `None` and `False` match against
|
||||
# any _value_ of sequence and mapping nodes. `True` matches against
|
||||
# any _key_ of a mapping node. A string `index_check` matches against
|
||||
# a mapping value that corresponds to a scalar key which content is
|
||||
# equal to the `index_check` value. An integer `index_check` matches
|
||||
# against a sequence value with the index equal to `index_check`.
|
||||
if 'yaml_path_resolvers' not in cls.__dict__:
|
||||
cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy()
|
||||
new_path = [] # type: List[Any]
|
||||
for element in path:
|
||||
if isinstance(element, (list, tuple)):
|
||||
if len(element) == 2:
|
||||
node_check, index_check = element
|
||||
elif len(element) == 1:
|
||||
node_check = element[0]
|
||||
index_check = True
|
||||
else:
|
||||
raise ResolverError(
|
||||
_F('Invalid path element: {element!s}', element=element)
|
||||
)
|
||||
else:
|
||||
node_check = None
|
||||
index_check = element
|
||||
if node_check is str:
|
||||
node_check = ScalarNode
|
||||
elif node_check is list:
|
||||
node_check = SequenceNode
|
||||
elif node_check is dict:
|
||||
node_check = MappingNode
|
||||
elif (
|
||||
node_check not in [ScalarNode, SequenceNode, MappingNode]
|
||||
and not isinstance(node_check, str)
|
||||
and node_check is not None
|
||||
):
|
||||
raise ResolverError(
|
||||
_F('Invalid node checker: {node_check!s}', node_check=node_check)
|
||||
)
|
||||
if not isinstance(index_check, (str, int)) and index_check is not None:
|
||||
raise ResolverError(
|
||||
_F('Invalid index checker: {index_check!s}', index_check=index_check)
|
||||
)
|
||||
new_path.append((node_check, index_check))
|
||||
if kind is str:
|
||||
kind = ScalarNode
|
||||
elif kind is list:
|
||||
kind = SequenceNode
|
||||
elif kind is dict:
|
||||
kind = MappingNode
|
||||
elif kind not in [ScalarNode, SequenceNode, MappingNode] and kind is not None:
|
||||
raise ResolverError(_F('Invalid node kind: {kind!s}', kind=kind))
|
||||
cls.yaml_path_resolvers[tuple(new_path), kind] = tag
|
||||
|
||||
def descend_resolver(self, current_node, current_index):
|
||||
# type: (Any, Any) -> None
|
||||
if not self.yaml_path_resolvers:
|
||||
return
|
||||
exact_paths = {}
|
||||
prefix_paths = []
|
||||
if current_node:
|
||||
depth = len(self.resolver_prefix_paths)
|
||||
for path, kind in self.resolver_prefix_paths[-1]:
|
||||
if self.check_resolver_prefix(depth, path, kind, current_node, current_index):
|
||||
if len(path) > depth:
|
||||
prefix_paths.append((path, kind))
|
||||
else:
|
||||
exact_paths[kind] = self.yaml_path_resolvers[path, kind]
|
||||
else:
|
||||
for path, kind in self.yaml_path_resolvers:
|
||||
if not path:
|
||||
exact_paths[kind] = self.yaml_path_resolvers[path, kind]
|
||||
else:
|
||||
prefix_paths.append((path, kind))
|
||||
self.resolver_exact_paths.append(exact_paths)
|
||||
self.resolver_prefix_paths.append(prefix_paths)
|
||||
|
||||
def ascend_resolver(self):
|
||||
# type: () -> None
|
||||
if not self.yaml_path_resolvers:
|
||||
return
|
||||
self.resolver_exact_paths.pop()
|
||||
self.resolver_prefix_paths.pop()
|
||||
|
||||
def check_resolver_prefix(self, depth, path, kind, current_node, current_index):
|
||||
# type: (int, Any, Any, Any, Any) -> bool
|
||||
node_check, index_check = path[depth - 1]
|
||||
if isinstance(node_check, str):
|
||||
if current_node.tag != node_check:
|
||||
return False
|
||||
elif node_check is not None:
|
||||
if not isinstance(current_node, node_check):
|
||||
return False
|
||||
if index_check is True and current_index is not None:
|
||||
return False
|
||||
if (index_check is False or index_check is None) and current_index is None:
|
||||
return False
|
||||
if isinstance(index_check, str):
|
||||
if not (
|
||||
isinstance(current_index, ScalarNode) and index_check == current_index.value
|
||||
):
|
||||
return False
|
||||
elif isinstance(index_check, int) and not isinstance(index_check, bool):
|
||||
if index_check != current_index:
|
||||
return False
|
||||
return True
|
||||
|
||||
def resolve(self, kind, value, implicit):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
if kind is ScalarNode and implicit[0]:
|
||||
if value == "":
|
||||
resolvers = self.yaml_implicit_resolvers.get("", [])
|
||||
else:
|
||||
resolvers = self.yaml_implicit_resolvers.get(value[0], [])
|
||||
resolvers += self.yaml_implicit_resolvers.get(None, [])
|
||||
for tag, regexp in resolvers:
|
||||
if regexp.match(value):
|
||||
return tag
|
||||
implicit = implicit[1]
|
||||
if bool(self.yaml_path_resolvers):
|
||||
exact_paths = self.resolver_exact_paths[-1]
|
||||
if kind in exact_paths:
|
||||
return exact_paths[kind]
|
||||
if None in exact_paths:
|
||||
return exact_paths[None]
|
||||
if kind is ScalarNode:
|
||||
return self.DEFAULT_SCALAR_TAG
|
||||
elif kind is SequenceNode:
|
||||
return self.DEFAULT_SEQUENCE_TAG
|
||||
elif kind is MappingNode:
|
||||
return self.DEFAULT_MAPPING_TAG
|
||||
|
||||
@property
|
||||
def processing_version(self):
|
||||
# type: () -> Any
|
||||
return None
|
||||
|
||||
|
||||
class Resolver(BaseResolver):
|
||||
pass
|
||||
|
||||
|
||||
for ir in implicit_resolvers:
|
||||
if (1, 2) in ir[0]:
|
||||
Resolver.add_implicit_resolver_base(*ir[1:])
|
||||
|
||||
|
||||
class VersionedResolver(BaseResolver):
|
||||
"""
|
||||
contrary to the "normal" resolver, the smart resolver delays loading
|
||||
the pattern matching rules. That way it can decide to load 1.1 rules
|
||||
or the (default) 1.2 rules, that no longer support octal without 0o, sexagesimals
|
||||
and Yes/No/On/Off booleans.
|
||||
"""
|
||||
|
||||
def __init__(self, version=None, loader=None, loadumper=None):
|
||||
# type: (Optional[VersionType], Any, Any) -> None
|
||||
if loader is None and loadumper is not None:
|
||||
loader = loadumper
|
||||
BaseResolver.__init__(self, loader)
|
||||
self._loader_version = self.get_loader_version(version)
|
||||
self._version_implicit_resolver = {} # type: Dict[Any, Any]
|
||||
|
||||
def add_version_implicit_resolver(self, version, tag, regexp, first):
|
||||
# type: (VersionType, Any, Any, Any) -> None
|
||||
if first is None:
|
||||
first = [None]
|
||||
impl_resolver = self._version_implicit_resolver.setdefault(version, {})
|
||||
for ch in first:
|
||||
impl_resolver.setdefault(ch, []).append((tag, regexp))
|
||||
|
||||
def get_loader_version(self, version):
|
||||
# type: (Optional[VersionType]) -> Any
|
||||
if version is None or isinstance(version, tuple):
|
||||
return version
|
||||
if isinstance(version, list):
|
||||
return tuple(version)
|
||||
# assume string
|
||||
return tuple(map(int, version.split('.')))
|
||||
|
||||
@property
|
||||
def versioned_resolver(self):
|
||||
# type: () -> Any
|
||||
"""
|
||||
select the resolver based on the version we are parsing
|
||||
"""
|
||||
version = self.processing_version
|
||||
if isinstance(version, str):
|
||||
version = tuple(map(int, version.split('.')))
|
||||
if version not in self._version_implicit_resolver:
|
||||
for x in implicit_resolvers:
|
||||
if version in x[0]:
|
||||
self.add_version_implicit_resolver(version, x[1], x[2], x[3])
|
||||
return self._version_implicit_resolver[version]
|
||||
|
||||
def resolve(self, kind, value, implicit):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
if kind is ScalarNode and implicit[0]:
|
||||
if value == "":
|
||||
resolvers = self.versioned_resolver.get("", [])
|
||||
else:
|
||||
resolvers = self.versioned_resolver.get(value[0], [])
|
||||
resolvers += self.versioned_resolver.get(None, [])
|
||||
for tag, regexp in resolvers:
|
||||
if regexp.match(value):
|
||||
return tag
|
||||
implicit = implicit[1]
|
||||
if bool(self.yaml_path_resolvers):
|
||||
exact_paths = self.resolver_exact_paths[-1]
|
||||
if kind in exact_paths:
|
||||
return exact_paths[kind]
|
||||
if None in exact_paths:
|
||||
return exact_paths[None]
|
||||
if kind is ScalarNode:
|
||||
return self.DEFAULT_SCALAR_TAG
|
||||
elif kind is SequenceNode:
|
||||
return self.DEFAULT_SEQUENCE_TAG
|
||||
elif kind is MappingNode:
|
||||
return self.DEFAULT_MAPPING_TAG
|
||||
|
||||
@property
|
||||
def processing_version(self):
|
||||
# type: () -> Any
|
||||
try:
|
||||
version = self.loadumper._scanner.yaml_version
|
||||
except AttributeError:
|
||||
try:
|
||||
if hasattr(self.loadumper, 'typ'):
|
||||
version = self.loadumper.version
|
||||
else:
|
||||
version = self.loadumper._serializer.use_version # dumping
|
||||
except AttributeError:
|
||||
version = None
|
||||
if version is None:
|
||||
version = self._loader_version
|
||||
if version is None:
|
||||
version = _DEFAULT_YAML_VERSION
|
||||
return version
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
You cannot subclass bool, and this is necessary for round-tripping anchored
|
||||
bool values (and also if you want to preserve the original way of writing)
|
||||
|
||||
bool.__bases__ is type 'int', so that is what is used as the basis for ScalarBoolean as well.
|
||||
|
||||
You can use these in an if statement, but not when testing equivalence
|
||||
"""
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.anchor import Anchor
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Text, Any, Dict, List # NOQA
|
||||
|
||||
__all__ = ['ScalarBoolean']
|
||||
|
||||
|
||||
class ScalarBoolean(int):
|
||||
def __new__(cls, *args, **kw):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
anchor = kw.pop('anchor', None)
|
||||
b = int.__new__(cls, *args, **kw)
|
||||
if anchor is not None:
|
||||
b.yaml_set_anchor(anchor, always_dump=True)
|
||||
return b
|
||||
|
||||
@property
|
||||
def anchor(self):
|
||||
# type: () -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
setattr(self, Anchor.attrib, Anchor())
|
||||
return getattr(self, Anchor.attrib)
|
||||
|
||||
def yaml_anchor(self, any=False):
|
||||
# type: (bool) -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
return None
|
||||
if any or self.anchor.always_dump:
|
||||
return self.anchor
|
||||
return None
|
||||
|
||||
def yaml_set_anchor(self, value, always_dump=False):
|
||||
# type: (Any, bool) -> None
|
||||
self.anchor.value = value
|
||||
self.anchor.always_dump = always_dump
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
# coding: utf-8
|
||||
|
||||
import sys
|
||||
from pipenv.vendor.ruamel.yaml.anchor import Anchor
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Text, Any, Dict, List # NOQA
|
||||
|
||||
__all__ = ['ScalarFloat', 'ExponentialFloat', 'ExponentialCapsFloat']
|
||||
|
||||
|
||||
class ScalarFloat(float):
|
||||
def __new__(cls, *args, **kw):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
width = kw.pop('width', None)
|
||||
prec = kw.pop('prec', None)
|
||||
m_sign = kw.pop('m_sign', None)
|
||||
m_lead0 = kw.pop('m_lead0', 0)
|
||||
exp = kw.pop('exp', None)
|
||||
e_width = kw.pop('e_width', None)
|
||||
e_sign = kw.pop('e_sign', None)
|
||||
underscore = kw.pop('underscore', None)
|
||||
anchor = kw.pop('anchor', None)
|
||||
v = float.__new__(cls, *args, **kw)
|
||||
v._width = width
|
||||
v._prec = prec
|
||||
v._m_sign = m_sign
|
||||
v._m_lead0 = m_lead0
|
||||
v._exp = exp
|
||||
v._e_width = e_width
|
||||
v._e_sign = e_sign
|
||||
v._underscore = underscore
|
||||
if anchor is not None:
|
||||
v.yaml_set_anchor(anchor, always_dump=True)
|
||||
return v
|
||||
|
||||
def __iadd__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
return float(self) + a
|
||||
x = type(self)(self + a)
|
||||
x._width = self._width
|
||||
x._underscore = self._underscore[:] if self._underscore is not None else None # NOQA
|
||||
return x
|
||||
|
||||
def __ifloordiv__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
return float(self) // a
|
||||
x = type(self)(self // a)
|
||||
x._width = self._width
|
||||
x._underscore = self._underscore[:] if self._underscore is not None else None # NOQA
|
||||
return x
|
||||
|
||||
def __imul__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
return float(self) * a
|
||||
x = type(self)(self * a)
|
||||
x._width = self._width
|
||||
x._underscore = self._underscore[:] if self._underscore is not None else None # NOQA
|
||||
x._prec = self._prec # check for others
|
||||
return x
|
||||
|
||||
def __ipow__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
return float(self) ** a
|
||||
x = type(self)(self ** a)
|
||||
x._width = self._width
|
||||
x._underscore = self._underscore[:] if self._underscore is not None else None # NOQA
|
||||
return x
|
||||
|
||||
def __isub__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
return float(self) - a
|
||||
x = type(self)(self - a)
|
||||
x._width = self._width
|
||||
x._underscore = self._underscore[:] if self._underscore is not None else None # NOQA
|
||||
return x
|
||||
|
||||
@property
|
||||
def anchor(self):
|
||||
# type: () -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
setattr(self, Anchor.attrib, Anchor())
|
||||
return getattr(self, Anchor.attrib)
|
||||
|
||||
def yaml_anchor(self, any=False):
|
||||
# type: (bool) -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
return None
|
||||
if any or self.anchor.always_dump:
|
||||
return self.anchor
|
||||
return None
|
||||
|
||||
def yaml_set_anchor(self, value, always_dump=False):
|
||||
# type: (Any, bool) -> None
|
||||
self.anchor.value = value
|
||||
self.anchor.always_dump = always_dump
|
||||
|
||||
def dump(self, out=sys.stdout):
|
||||
# type: (Any) -> Any
|
||||
out.write(
|
||||
'ScalarFloat({}| w:{}, p:{}, s:{}, lz:{}, _:{}|{}, w:{}, s:{})\n'.format(
|
||||
self,
|
||||
self._width, # type: ignore
|
||||
self._prec, # type: ignore
|
||||
self._m_sign, # type: ignore
|
||||
self._m_lead0, # type: ignore
|
||||
self._underscore, # type: ignore
|
||||
self._exp, # type: ignore
|
||||
self._e_width, # type: ignore
|
||||
self._e_sign, # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ExponentialFloat(ScalarFloat):
|
||||
def __new__(cls, value, width=None, underscore=None):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
return ScalarFloat.__new__(cls, value, width=width, underscore=underscore)
|
||||
|
||||
|
||||
class ExponentialCapsFloat(ScalarFloat):
|
||||
def __new__(cls, value, width=None, underscore=None):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
return ScalarFloat.__new__(cls, value, width=width, underscore=underscore)
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
# coding: utf-8
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.anchor import Anchor
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Text, Any, Dict, List # NOQA
|
||||
|
||||
__all__ = ['ScalarInt', 'BinaryInt', 'OctalInt', 'HexInt', 'HexCapsInt', 'DecimalInt']
|
||||
|
||||
|
||||
class ScalarInt(int):
|
||||
def __new__(cls, *args, **kw):
|
||||
# type: (Any, Any, Any) -> Any
|
||||
width = kw.pop('width', None)
|
||||
underscore = kw.pop('underscore', None)
|
||||
anchor = kw.pop('anchor', None)
|
||||
v = int.__new__(cls, *args, **kw)
|
||||
v._width = width
|
||||
v._underscore = underscore
|
||||
if anchor is not None:
|
||||
v.yaml_set_anchor(anchor, always_dump=True)
|
||||
return v
|
||||
|
||||
def __iadd__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
x = type(self)(self + a)
|
||||
x._width = self._width # type: ignore
|
||||
x._underscore = ( # type: ignore
|
||||
self._underscore[:] if self._underscore is not None else None # type: ignore
|
||||
) # NOQA
|
||||
return x
|
||||
|
||||
def __ifloordiv__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
x = type(self)(self // a)
|
||||
x._width = self._width # type: ignore
|
||||
x._underscore = ( # type: ignore
|
||||
self._underscore[:] if self._underscore is not None else None # type: ignore
|
||||
) # NOQA
|
||||
return x
|
||||
|
||||
def __imul__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
x = type(self)(self * a)
|
||||
x._width = self._width # type: ignore
|
||||
x._underscore = ( # type: ignore
|
||||
self._underscore[:] if self._underscore is not None else None # type: ignore
|
||||
) # NOQA
|
||||
return x
|
||||
|
||||
def __ipow__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
x = type(self)(self ** a)
|
||||
x._width = self._width # type: ignore
|
||||
x._underscore = ( # type: ignore
|
||||
self._underscore[:] if self._underscore is not None else None # type: ignore
|
||||
) # NOQA
|
||||
return x
|
||||
|
||||
def __isub__(self, a): # type: ignore
|
||||
# type: (Any) -> Any
|
||||
x = type(self)(self - a)
|
||||
x._width = self._width # type: ignore
|
||||
x._underscore = ( # type: ignore
|
||||
self._underscore[:] if self._underscore is not None else None # type: ignore
|
||||
) # NOQA
|
||||
return x
|
||||
|
||||
@property
|
||||
def anchor(self):
|
||||
# type: () -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
setattr(self, Anchor.attrib, Anchor())
|
||||
return getattr(self, Anchor.attrib)
|
||||
|
||||
def yaml_anchor(self, any=False):
|
||||
# type: (bool) -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
return None
|
||||
if any or self.anchor.always_dump:
|
||||
return self.anchor
|
||||
return None
|
||||
|
||||
def yaml_set_anchor(self, value, always_dump=False):
|
||||
# type: (Any, bool) -> None
|
||||
self.anchor.value = value
|
||||
self.anchor.always_dump = always_dump
|
||||
|
||||
|
||||
class BinaryInt(ScalarInt):
|
||||
def __new__(cls, value, width=None, underscore=None, anchor=None):
|
||||
# type: (Any, Any, Any, Any) -> Any
|
||||
return ScalarInt.__new__(cls, value, width=width, underscore=underscore, anchor=anchor)
|
||||
|
||||
|
||||
class OctalInt(ScalarInt):
|
||||
def __new__(cls, value, width=None, underscore=None, anchor=None):
|
||||
# type: (Any, Any, Any, Any) -> Any
|
||||
return ScalarInt.__new__(cls, value, width=width, underscore=underscore, anchor=anchor)
|
||||
|
||||
|
||||
# mixed casing of A-F is not supported, when loading the first non digit
|
||||
# determines the case
|
||||
|
||||
|
||||
class HexInt(ScalarInt):
|
||||
"""uses lower case (a-f)"""
|
||||
|
||||
def __new__(cls, value, width=None, underscore=None, anchor=None):
|
||||
# type: (Any, Any, Any, Any) -> Any
|
||||
return ScalarInt.__new__(cls, value, width=width, underscore=underscore, anchor=anchor)
|
||||
|
||||
|
||||
class HexCapsInt(ScalarInt):
|
||||
"""uses upper case (A-F)"""
|
||||
|
||||
def __new__(cls, value, width=None, underscore=None, anchor=None):
|
||||
# type: (Any, Any, Any, Any) -> Any
|
||||
return ScalarInt.__new__(cls, value, width=width, underscore=underscore, anchor=anchor)
|
||||
|
||||
|
||||
class DecimalInt(ScalarInt):
|
||||
"""needed if anchor"""
|
||||
|
||||
def __new__(cls, value, width=None, underscore=None, anchor=None):
|
||||
# type: (Any, Any, Any, Any) -> Any
|
||||
return ScalarInt.__new__(cls, value, width=width, underscore=underscore, anchor=anchor)
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# coding: utf-8
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.anchor import Anchor
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Text, Any, Dict, List # NOQA
|
||||
|
||||
__all__ = [
|
||||
'ScalarString',
|
||||
'LiteralScalarString',
|
||||
'FoldedScalarString',
|
||||
'SingleQuotedScalarString',
|
||||
'DoubleQuotedScalarString',
|
||||
'PlainScalarString',
|
||||
# PreservedScalarString is the old name, as it was the first to be preserved on rt,
|
||||
# use LiteralScalarString instead
|
||||
'PreservedScalarString',
|
||||
]
|
||||
|
||||
|
||||
class ScalarString(str):
|
||||
__slots__ = Anchor.attrib
|
||||
|
||||
def __new__(cls, *args, **kw):
|
||||
# type: (Any, Any) -> Any
|
||||
anchor = kw.pop('anchor', None)
|
||||
ret_val = str.__new__(cls, *args, **kw)
|
||||
if anchor is not None:
|
||||
ret_val.yaml_set_anchor(anchor, always_dump=True)
|
||||
return ret_val
|
||||
|
||||
def replace(self, old, new, maxreplace=-1):
|
||||
# type: (Any, Any, int) -> Any
|
||||
return type(self)((str.replace(self, old, new, maxreplace)))
|
||||
|
||||
@property
|
||||
def anchor(self):
|
||||
# type: () -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
setattr(self, Anchor.attrib, Anchor())
|
||||
return getattr(self, Anchor.attrib)
|
||||
|
||||
def yaml_anchor(self, any=False):
|
||||
# type: (bool) -> Any
|
||||
if not hasattr(self, Anchor.attrib):
|
||||
return None
|
||||
if any or self.anchor.always_dump:
|
||||
return self.anchor
|
||||
return None
|
||||
|
||||
def yaml_set_anchor(self, value, always_dump=False):
|
||||
# type: (Any, bool) -> None
|
||||
self.anchor.value = value
|
||||
self.anchor.always_dump = always_dump
|
||||
|
||||
|
||||
class LiteralScalarString(ScalarString):
|
||||
__slots__ = 'comment' # the comment after the | on the first line
|
||||
|
||||
style = '|'
|
||||
|
||||
def __new__(cls, value, anchor=None):
|
||||
# type: (Text, Any) -> Any
|
||||
return ScalarString.__new__(cls, value, anchor=anchor)
|
||||
|
||||
|
||||
PreservedScalarString = LiteralScalarString
|
||||
|
||||
|
||||
class FoldedScalarString(ScalarString):
|
||||
__slots__ = ('fold_pos', 'comment') # the comment after the > on the first line
|
||||
|
||||
style = '>'
|
||||
|
||||
def __new__(cls, value, anchor=None):
|
||||
# type: (Text, Any) -> Any
|
||||
return ScalarString.__new__(cls, value, anchor=anchor)
|
||||
|
||||
|
||||
class SingleQuotedScalarString(ScalarString):
|
||||
__slots__ = ()
|
||||
|
||||
style = "'"
|
||||
|
||||
def __new__(cls, value, anchor=None):
|
||||
# type: (Text, Any) -> Any
|
||||
return ScalarString.__new__(cls, value, anchor=anchor)
|
||||
|
||||
|
||||
class DoubleQuotedScalarString(ScalarString):
|
||||
__slots__ = ()
|
||||
|
||||
style = '"'
|
||||
|
||||
def __new__(cls, value, anchor=None):
|
||||
# type: (Text, Any) -> Any
|
||||
return ScalarString.__new__(cls, value, anchor=anchor)
|
||||
|
||||
|
||||
class PlainScalarString(ScalarString):
|
||||
__slots__ = ()
|
||||
|
||||
style = ''
|
||||
|
||||
def __new__(cls, value, anchor=None):
|
||||
# type: (Text, Any) -> Any
|
||||
return ScalarString.__new__(cls, value, anchor=anchor)
|
||||
|
||||
|
||||
def preserve_literal(s):
|
||||
# type: (Text) -> Text
|
||||
return LiteralScalarString(s.replace('\r\n', '\n').replace('\r', '\n'))
|
||||
|
||||
|
||||
def walk_tree(base, map=None):
|
||||
# type: (Any, Any) -> None
|
||||
"""
|
||||
the routine here walks over a simple yaml tree (recursing in
|
||||
dict values and list items) and converts strings that
|
||||
have multiple lines to literal scalars
|
||||
|
||||
You can also provide an explicit (ordered) mapping for multiple transforms
|
||||
(first of which is executed):
|
||||
map = ruamel.compat.ordereddict
|
||||
map['\n'] = preserve_literal
|
||||
map[':'] = SingleQuotedScalarString
|
||||
walk_tree(data, map=map)
|
||||
"""
|
||||
from collections.abc import MutableMapping, MutableSequence
|
||||
|
||||
if map is None:
|
||||
map = {'\n': preserve_literal}
|
||||
|
||||
if isinstance(base, MutableMapping):
|
||||
for k in base:
|
||||
v = base[k] # type: Text
|
||||
if isinstance(v, str):
|
||||
for ch in map:
|
||||
if ch in v:
|
||||
base[k] = map[ch](v)
|
||||
break
|
||||
else:
|
||||
walk_tree(v, map=map)
|
||||
elif isinstance(base, MutableSequence):
|
||||
for idx, elem in enumerate(base):
|
||||
if isinstance(elem, str):
|
||||
for ch in map:
|
||||
if ch in elem:
|
||||
base[idx] = map[ch](elem)
|
||||
break
|
||||
else:
|
||||
walk_tree(elem, map=map)
|
||||
Vendored
+2444
File diff suppressed because it is too large
Load Diff
+241
@@ -0,0 +1,241 @@
|
||||
# coding: utf-8
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.error import YAMLError
|
||||
from pipenv.vendor.ruamel.yaml.compat import nprint, DBG_NODE, dbg, nprintf # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.util import RegExp
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.events import (
|
||||
StreamStartEvent,
|
||||
StreamEndEvent,
|
||||
MappingStartEvent,
|
||||
MappingEndEvent,
|
||||
SequenceStartEvent,
|
||||
SequenceEndEvent,
|
||||
AliasEvent,
|
||||
ScalarEvent,
|
||||
DocumentStartEvent,
|
||||
DocumentEndEvent,
|
||||
)
|
||||
from pipenv.vendor.ruamel.yaml.nodes import MappingNode, ScalarNode, SequenceNode
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Union, Text, Optional # NOQA
|
||||
from pipenv.vendor.ruamel.yaml.compat import VersionType # NOQA
|
||||
|
||||
__all__ = ['Serializer', 'SerializerError']
|
||||
|
||||
|
||||
class SerializerError(YAMLError):
|
||||
pass
|
||||
|
||||
|
||||
class Serializer:
|
||||
|
||||
# 'id' and 3+ numbers, but not 000
|
||||
ANCHOR_TEMPLATE = 'id%03d'
|
||||
ANCHOR_RE = RegExp('id(?!000$)\\d{3,}')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
encoding=None,
|
||||
explicit_start=None,
|
||||
explicit_end=None,
|
||||
version=None,
|
||||
tags=None,
|
||||
dumper=None,
|
||||
):
|
||||
# type: (Any, Optional[bool], Optional[bool], Optional[VersionType], Any, Any) -> None # NOQA
|
||||
self.dumper = dumper
|
||||
if self.dumper is not None:
|
||||
self.dumper._serializer = self
|
||||
self.use_encoding = encoding
|
||||
self.use_explicit_start = explicit_start
|
||||
self.use_explicit_end = explicit_end
|
||||
if isinstance(version, str):
|
||||
self.use_version = tuple(map(int, version.split('.')))
|
||||
else:
|
||||
self.use_version = version # type: ignore
|
||||
self.use_tags = tags
|
||||
self.serialized_nodes = {} # type: Dict[Any, Any]
|
||||
self.anchors = {} # type: Dict[Any, Any]
|
||||
self.last_anchor_id = 0
|
||||
self.closed = None # type: Optional[bool]
|
||||
self._templated_id = None
|
||||
|
||||
@property
|
||||
def emitter(self):
|
||||
# type: () -> Any
|
||||
if hasattr(self.dumper, 'typ'):
|
||||
return self.dumper.emitter
|
||||
return self.dumper._emitter
|
||||
|
||||
@property
|
||||
def resolver(self):
|
||||
# type: () -> Any
|
||||
if hasattr(self.dumper, 'typ'):
|
||||
self.dumper.resolver
|
||||
return self.dumper._resolver
|
||||
|
||||
def open(self):
|
||||
# type: () -> None
|
||||
if self.closed is None:
|
||||
self.emitter.emit(StreamStartEvent(encoding=self.use_encoding))
|
||||
self.closed = False
|
||||
elif self.closed:
|
||||
raise SerializerError('serializer is closed')
|
||||
else:
|
||||
raise SerializerError('serializer is already opened')
|
||||
|
||||
def close(self):
|
||||
# type: () -> None
|
||||
if self.closed is None:
|
||||
raise SerializerError('serializer is not opened')
|
||||
elif not self.closed:
|
||||
self.emitter.emit(StreamEndEvent())
|
||||
self.closed = True
|
||||
|
||||
# def __del__(self):
|
||||
# self.close()
|
||||
|
||||
def serialize(self, node):
|
||||
# type: (Any) -> None
|
||||
if dbg(DBG_NODE):
|
||||
nprint('Serializing nodes')
|
||||
node.dump()
|
||||
if self.closed is None:
|
||||
raise SerializerError('serializer is not opened')
|
||||
elif self.closed:
|
||||
raise SerializerError('serializer is closed')
|
||||
self.emitter.emit(
|
||||
DocumentStartEvent(
|
||||
explicit=self.use_explicit_start, version=self.use_version, tags=self.use_tags
|
||||
)
|
||||
)
|
||||
self.anchor_node(node)
|
||||
self.serialize_node(node, None, None)
|
||||
self.emitter.emit(DocumentEndEvent(explicit=self.use_explicit_end))
|
||||
self.serialized_nodes = {}
|
||||
self.anchors = {}
|
||||
self.last_anchor_id = 0
|
||||
|
||||
def anchor_node(self, node):
|
||||
# type: (Any) -> None
|
||||
if node in self.anchors:
|
||||
if self.anchors[node] is None:
|
||||
self.anchors[node] = self.generate_anchor(node)
|
||||
else:
|
||||
anchor = None
|
||||
try:
|
||||
if node.anchor.always_dump:
|
||||
anchor = node.anchor.value
|
||||
except: # NOQA
|
||||
pass
|
||||
self.anchors[node] = anchor
|
||||
if isinstance(node, SequenceNode):
|
||||
for item in node.value:
|
||||
self.anchor_node(item)
|
||||
elif isinstance(node, MappingNode):
|
||||
for key, value in node.value:
|
||||
self.anchor_node(key)
|
||||
self.anchor_node(value)
|
||||
|
||||
def generate_anchor(self, node):
|
||||
# type: (Any) -> Any
|
||||
try:
|
||||
anchor = node.anchor.value
|
||||
except: # NOQA
|
||||
anchor = None
|
||||
if anchor is None:
|
||||
self.last_anchor_id += 1
|
||||
return self.ANCHOR_TEMPLATE % self.last_anchor_id
|
||||
return anchor
|
||||
|
||||
def serialize_node(self, node, parent, index):
|
||||
# type: (Any, Any, Any) -> None
|
||||
alias = self.anchors[node]
|
||||
if node in self.serialized_nodes:
|
||||
node_style = getattr(node, 'style', None)
|
||||
if node_style != '?':
|
||||
node_style = None
|
||||
self.emitter.emit(AliasEvent(alias, style=node_style))
|
||||
else:
|
||||
self.serialized_nodes[node] = True
|
||||
self.resolver.descend_resolver(parent, index)
|
||||
if isinstance(node, ScalarNode):
|
||||
# here check if the node.tag equals the one that would result from parsing
|
||||
# if not equal quoting is necessary for strings
|
||||
detected_tag = self.resolver.resolve(ScalarNode, node.value, (True, False))
|
||||
default_tag = self.resolver.resolve(ScalarNode, node.value, (False, True))
|
||||
implicit = (
|
||||
(node.tag == detected_tag),
|
||||
(node.tag == default_tag),
|
||||
node.tag.startswith('tag:yaml.org,2002:'),
|
||||
)
|
||||
self.emitter.emit(
|
||||
ScalarEvent(
|
||||
alias,
|
||||
node.tag,
|
||||
implicit,
|
||||
node.value,
|
||||
style=node.style,
|
||||
comment=node.comment,
|
||||
)
|
||||
)
|
||||
elif isinstance(node, SequenceNode):
|
||||
implicit = node.tag == self.resolver.resolve(SequenceNode, node.value, True)
|
||||
comment = node.comment
|
||||
end_comment = None
|
||||
seq_comment = None
|
||||
if node.flow_style is True:
|
||||
if comment: # eol comment on flow style sequence
|
||||
seq_comment = comment[0]
|
||||
# comment[0] = None
|
||||
if comment and len(comment) > 2:
|
||||
end_comment = comment[2]
|
||||
else:
|
||||
end_comment = None
|
||||
self.emitter.emit(
|
||||
SequenceStartEvent(
|
||||
alias,
|
||||
node.tag,
|
||||
implicit,
|
||||
flow_style=node.flow_style,
|
||||
comment=node.comment,
|
||||
)
|
||||
)
|
||||
index = 0
|
||||
for item in node.value:
|
||||
self.serialize_node(item, node, index)
|
||||
index += 1
|
||||
self.emitter.emit(SequenceEndEvent(comment=[seq_comment, end_comment]))
|
||||
elif isinstance(node, MappingNode):
|
||||
implicit = node.tag == self.resolver.resolve(MappingNode, node.value, True)
|
||||
comment = node.comment
|
||||
end_comment = None
|
||||
map_comment = None
|
||||
if node.flow_style is True:
|
||||
if comment: # eol comment on flow style sequence
|
||||
map_comment = comment[0]
|
||||
# comment[0] = None
|
||||
if comment and len(comment) > 2:
|
||||
end_comment = comment[2]
|
||||
self.emitter.emit(
|
||||
MappingStartEvent(
|
||||
alias,
|
||||
node.tag,
|
||||
implicit,
|
||||
flow_style=node.flow_style,
|
||||
comment=node.comment,
|
||||
nr_items=len(node.value),
|
||||
)
|
||||
)
|
||||
for key, value in node.value:
|
||||
self.serialize_node(key, node, None)
|
||||
self.serialize_node(value, node, key)
|
||||
self.emitter.emit(MappingEndEvent(comment=[map_comment, end_comment]))
|
||||
self.resolver.ascend_resolver()
|
||||
|
||||
|
||||
def templated_id(s):
|
||||
# type: (Text) -> Any
|
||||
return Serializer.ANCHOR_RE.match(s)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# coding: utf-8
|
||||
|
||||
import datetime
|
||||
import copy
|
||||
|
||||
# ToDo: at least on PY3 you could probably attach the tzinfo correctly to the object
|
||||
# a more complete datetime might be used by safe loading as well
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List # NOQA
|
||||
|
||||
|
||||
class TimeStamp(datetime.datetime):
|
||||
def __init__(self, *args, **kw):
|
||||
# type: (Any, Any) -> None
|
||||
self._yaml = dict(t=False, tz=None, delta=0) # type: Dict[Any, Any]
|
||||
|
||||
def __new__(cls, *args, **kw): # datetime is immutable
|
||||
# type: (Any, Any) -> Any
|
||||
return datetime.datetime.__new__(cls, *args, **kw)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
# type: (Any) -> Any
|
||||
ts = TimeStamp(self.year, self.month, self.day, self.hour, self.minute, self.second)
|
||||
ts._yaml = copy.deepcopy(self._yaml)
|
||||
return ts
|
||||
|
||||
def replace(
|
||||
self,
|
||||
year=None,
|
||||
month=None,
|
||||
day=None,
|
||||
hour=None,
|
||||
minute=None,
|
||||
second=None,
|
||||
microsecond=None,
|
||||
tzinfo=True,
|
||||
fold=None,
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any, Any, Any, Any) -> Any
|
||||
if year is None:
|
||||
year = self.year
|
||||
if month is None:
|
||||
month = self.month
|
||||
if day is None:
|
||||
day = self.day
|
||||
if hour is None:
|
||||
hour = self.hour
|
||||
if minute is None:
|
||||
minute = self.minute
|
||||
if second is None:
|
||||
second = self.second
|
||||
if microsecond is None:
|
||||
microsecond = self.microsecond
|
||||
if tzinfo is True:
|
||||
tzinfo = self.tzinfo
|
||||
if fold is None:
|
||||
fold = self.fold
|
||||
ts = type(self)(year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold)
|
||||
ts._yaml = copy.deepcopy(self._yaml)
|
||||
return ts
|
||||
Vendored
+404
@@ -0,0 +1,404 @@
|
||||
# coding: utf-8
|
||||
|
||||
from pipenv.vendor.ruamel.yaml.compat import _F, nprintf # NOQA
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Text, Any, Dict, Optional, List # NOQA
|
||||
from .error import StreamMark # NOQA
|
||||
|
||||
SHOW_LINES = True
|
||||
|
||||
|
||||
class Token:
|
||||
__slots__ = 'start_mark', 'end_mark', '_comment'
|
||||
|
||||
def __init__(self, start_mark, end_mark):
|
||||
# type: (StreamMark, StreamMark) -> None
|
||||
self.start_mark = start_mark
|
||||
self.end_mark = end_mark
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> Any
|
||||
# attributes = [key for key in self.__slots__ if not key.endswith('_mark') and
|
||||
# hasattr('self', key)]
|
||||
attributes = [key for key in self.__slots__ if not key.endswith('_mark')]
|
||||
attributes.sort()
|
||||
# arguments = ', '.join(
|
||||
# [_F('{key!s}={gattr!r})', key=key, gattr=getattr(self, key)) for key in attributes]
|
||||
# )
|
||||
arguments = [
|
||||
_F('{key!s}={gattr!r}', key=key, gattr=getattr(self, key)) for key in attributes
|
||||
]
|
||||
if SHOW_LINES:
|
||||
try:
|
||||
arguments.append('line: ' + str(self.start_mark.line))
|
||||
except: # NOQA
|
||||
pass
|
||||
try:
|
||||
arguments.append('comment: ' + str(self._comment))
|
||||
except: # NOQA
|
||||
pass
|
||||
return '{}({})'.format(self.__class__.__name__, ', '.join(arguments))
|
||||
|
||||
@property
|
||||
def column(self):
|
||||
# type: () -> int
|
||||
return self.start_mark.column
|
||||
|
||||
@column.setter
|
||||
def column(self, pos):
|
||||
# type: (Any) -> None
|
||||
self.start_mark.column = pos
|
||||
|
||||
# old style ( <= 0.17) is a TWO element list with first being the EOL
|
||||
# comment concatenated with following FLC/BLNK; and second being a list of FLC/BLNK
|
||||
# preceding the token
|
||||
# new style ( >= 0.17 ) is a THREE element list with the first being a list of
|
||||
# preceding FLC/BLNK, the second EOL and the third following FLC/BLNK
|
||||
# note that new style has differing order, and does not consist of CommentToken(s)
|
||||
# but of CommentInfo instances
|
||||
# any non-assigned values in new style are None, but first and last can be empty list
|
||||
# new style routines add one comment at a time
|
||||
|
||||
# going to be deprecated in favour of add_comment_eol/post
|
||||
def add_post_comment(self, comment):
|
||||
# type: (Any) -> None
|
||||
if not hasattr(self, '_comment'):
|
||||
self._comment = [None, None]
|
||||
else:
|
||||
assert len(self._comment) in [2, 5] # make sure it is version 0
|
||||
# if isinstance(comment, CommentToken):
|
||||
# if comment.value.startswith('# C09'):
|
||||
# raise
|
||||
self._comment[0] = comment
|
||||
|
||||
# going to be deprecated in favour of add_comment_pre
|
||||
def add_pre_comments(self, comments):
|
||||
# type: (Any) -> None
|
||||
if not hasattr(self, '_comment'):
|
||||
self._comment = [None, None]
|
||||
else:
|
||||
assert len(self._comment) == 2 # make sure it is version 0
|
||||
assert self._comment[1] is None
|
||||
self._comment[1] = comments
|
||||
return
|
||||
|
||||
# new style
|
||||
def add_comment_pre(self, comment):
|
||||
# type: (Any) -> None
|
||||
if not hasattr(self, '_comment'):
|
||||
self._comment = [[], None, None] # type: ignore
|
||||
else:
|
||||
assert len(self._comment) == 3
|
||||
if self._comment[0] is None:
|
||||
self._comment[0] = [] # type: ignore
|
||||
self._comment[0].append(comment) # type: ignore
|
||||
|
||||
def add_comment_eol(self, comment, comment_type):
|
||||
# type: (Any, Any) -> None
|
||||
if not hasattr(self, '_comment'):
|
||||
self._comment = [None, None, None]
|
||||
else:
|
||||
assert len(self._comment) == 3
|
||||
assert self._comment[1] is None
|
||||
if self.comment[1] is None:
|
||||
self._comment[1] = [] # type: ignore
|
||||
self._comment[1].extend([None] * (comment_type + 1 - len(self.comment[1]))) # type: ignore # NOQA
|
||||
# nprintf('commy', self.comment, comment_type)
|
||||
self._comment[1][comment_type] = comment # type: ignore
|
||||
|
||||
def add_comment_post(self, comment):
|
||||
# type: (Any) -> None
|
||||
if not hasattr(self, '_comment'):
|
||||
self._comment = [None, None, []] # type: ignore
|
||||
else:
|
||||
assert len(self._comment) == 3
|
||||
if self._comment[2] is None:
|
||||
self._comment[2] = [] # type: ignore
|
||||
self._comment[2].append(comment) # type: ignore
|
||||
|
||||
# def get_comment(self):
|
||||
# # type: () -> Any
|
||||
# return getattr(self, '_comment', None)
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
# type: () -> Any
|
||||
return getattr(self, '_comment', None)
|
||||
|
||||
def move_old_comment(self, target, empty=False):
|
||||
# type: (Any, bool) -> Any
|
||||
"""move a comment from this token to target (normally next token)
|
||||
used to combine e.g. comments before a BlockEntryToken to the
|
||||
ScalarToken that follows it
|
||||
empty is a special for empty values -> comment after key
|
||||
"""
|
||||
c = self.comment
|
||||
if c is None:
|
||||
return
|
||||
# don't push beyond last element
|
||||
if isinstance(target, (StreamEndToken, DocumentStartToken)):
|
||||
return
|
||||
delattr(self, '_comment')
|
||||
tc = target.comment
|
||||
if not tc: # target comment, just insert
|
||||
# special for empty value in key: value issue 25
|
||||
if empty:
|
||||
c = [c[0], c[1], None, None, c[0]]
|
||||
target._comment = c
|
||||
# nprint('mco2:', self, target, target.comment, empty)
|
||||
return self
|
||||
if c[0] and tc[0] or c[1] and tc[1]:
|
||||
raise NotImplementedError(_F('overlap in comment {c!r} {tc!r}', c=c, tc=tc))
|
||||
if c[0]:
|
||||
tc[0] = c[0]
|
||||
if c[1]:
|
||||
tc[1] = c[1]
|
||||
return self
|
||||
|
||||
def split_old_comment(self):
|
||||
# type: () -> Any
|
||||
""" split the post part of a comment, and return it
|
||||
as comment to be added. Delete second part if [None, None]
|
||||
abc: # this goes to sequence
|
||||
# this goes to first element
|
||||
- first element
|
||||
"""
|
||||
comment = self.comment
|
||||
if comment is None or comment[0] is None:
|
||||
return None # nothing to do
|
||||
ret_val = [comment[0], None]
|
||||
if comment[1] is None:
|
||||
delattr(self, '_comment')
|
||||
return ret_val
|
||||
|
||||
def move_new_comment(self, target, empty=False):
|
||||
# type: (Any, bool) -> Any
|
||||
"""move a comment from this token to target (normally next token)
|
||||
used to combine e.g. comments before a BlockEntryToken to the
|
||||
ScalarToken that follows it
|
||||
empty is a special for empty values -> comment after key
|
||||
"""
|
||||
c = self.comment
|
||||
if c is None:
|
||||
return
|
||||
# don't push beyond last element
|
||||
if isinstance(target, (StreamEndToken, DocumentStartToken)):
|
||||
return
|
||||
delattr(self, '_comment')
|
||||
tc = target.comment
|
||||
if not tc: # target comment, just insert
|
||||
# special for empty value in key: value issue 25
|
||||
if empty:
|
||||
c = [c[0], c[1], c[2]]
|
||||
target._comment = c
|
||||
# nprint('mco2:', self, target, target.comment, empty)
|
||||
return self
|
||||
# if self and target have both pre, eol or post comments, something seems wrong
|
||||
for idx in range(3):
|
||||
if c[idx] is not None and tc[idx] is not None:
|
||||
raise NotImplementedError(_F('overlap in comment {c!r} {tc!r}', c=c, tc=tc))
|
||||
# move the comment parts
|
||||
for idx in range(3):
|
||||
if c[idx]:
|
||||
tc[idx] = c[idx]
|
||||
return self
|
||||
|
||||
|
||||
# class BOMToken(Token):
|
||||
# id = '<byte order mark>'
|
||||
|
||||
|
||||
class DirectiveToken(Token):
|
||||
__slots__ = 'name', 'value'
|
||||
id = '<directive>'
|
||||
|
||||
def __init__(self, name, value, start_mark, end_mark):
|
||||
# type: (Any, Any, Any, Any) -> None
|
||||
Token.__init__(self, start_mark, end_mark)
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
|
||||
class DocumentStartToken(Token):
|
||||
__slots__ = ()
|
||||
id = '<document start>'
|
||||
|
||||
|
||||
class DocumentEndToken(Token):
|
||||
__slots__ = ()
|
||||
id = '<document end>'
|
||||
|
||||
|
||||
class StreamStartToken(Token):
|
||||
__slots__ = ('encoding',)
|
||||
id = '<stream start>'
|
||||
|
||||
def __init__(self, start_mark=None, end_mark=None, encoding=None):
|
||||
# type: (Any, Any, Any) -> None
|
||||
Token.__init__(self, start_mark, end_mark)
|
||||
self.encoding = encoding
|
||||
|
||||
|
||||
class StreamEndToken(Token):
|
||||
__slots__ = ()
|
||||
id = '<stream end>'
|
||||
|
||||
|
||||
class BlockSequenceStartToken(Token):
|
||||
__slots__ = ()
|
||||
id = '<block sequence start>'
|
||||
|
||||
|
||||
class BlockMappingStartToken(Token):
|
||||
__slots__ = ()
|
||||
id = '<block mapping start>'
|
||||
|
||||
|
||||
class BlockEndToken(Token):
|
||||
__slots__ = ()
|
||||
id = '<block end>'
|
||||
|
||||
|
||||
class FlowSequenceStartToken(Token):
|
||||
__slots__ = ()
|
||||
id = '['
|
||||
|
||||
|
||||
class FlowMappingStartToken(Token):
|
||||
__slots__ = ()
|
||||
id = '{'
|
||||
|
||||
|
||||
class FlowSequenceEndToken(Token):
|
||||
__slots__ = ()
|
||||
id = ']'
|
||||
|
||||
|
||||
class FlowMappingEndToken(Token):
|
||||
__slots__ = ()
|
||||
id = '}'
|
||||
|
||||
|
||||
class KeyToken(Token):
|
||||
__slots__ = ()
|
||||
id = '?'
|
||||
|
||||
# def x__repr__(self):
|
||||
# return 'KeyToken({})'.format(
|
||||
# self.start_mark.buffer[self.start_mark.index:].split(None, 1)[0])
|
||||
|
||||
|
||||
class ValueToken(Token):
|
||||
__slots__ = ()
|
||||
id = ':'
|
||||
|
||||
|
||||
class BlockEntryToken(Token):
|
||||
__slots__ = ()
|
||||
id = '-'
|
||||
|
||||
|
||||
class FlowEntryToken(Token):
|
||||
__slots__ = ()
|
||||
id = ','
|
||||
|
||||
|
||||
class AliasToken(Token):
|
||||
__slots__ = ('value',)
|
||||
id = '<alias>'
|
||||
|
||||
def __init__(self, value, start_mark, end_mark):
|
||||
# type: (Any, Any, Any) -> None
|
||||
Token.__init__(self, start_mark, end_mark)
|
||||
self.value = value
|
||||
|
||||
|
||||
class AnchorToken(Token):
|
||||
__slots__ = ('value',)
|
||||
id = '<anchor>'
|
||||
|
||||
def __init__(self, value, start_mark, end_mark):
|
||||
# type: (Any, Any, Any) -> None
|
||||
Token.__init__(self, start_mark, end_mark)
|
||||
self.value = value
|
||||
|
||||
|
||||
class TagToken(Token):
|
||||
__slots__ = ('value',)
|
||||
id = '<tag>'
|
||||
|
||||
def __init__(self, value, start_mark, end_mark):
|
||||
# type: (Any, Any, Any) -> None
|
||||
Token.__init__(self, start_mark, end_mark)
|
||||
self.value = value
|
||||
|
||||
|
||||
class ScalarToken(Token):
|
||||
__slots__ = 'value', 'plain', 'style'
|
||||
id = '<scalar>'
|
||||
|
||||
def __init__(self, value, plain, start_mark, end_mark, style=None):
|
||||
# type: (Any, Any, Any, Any, Any) -> None
|
||||
Token.__init__(self, start_mark, end_mark)
|
||||
self.value = value
|
||||
self.plain = plain
|
||||
self.style = style
|
||||
|
||||
|
||||
class CommentToken(Token):
|
||||
__slots__ = '_value', 'pre_done'
|
||||
id = '<comment>'
|
||||
|
||||
def __init__(self, value, start_mark=None, end_mark=None, column=None):
|
||||
# type: (Any, Any, Any, Any) -> None
|
||||
if start_mark is None:
|
||||
assert column is not None
|
||||
self._column = column
|
||||
Token.__init__(self, start_mark, None) # type: ignore
|
||||
self._value = value
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
# type: () -> str
|
||||
if isinstance(self._value, str):
|
||||
return self._value
|
||||
return "".join(self._value)
|
||||
|
||||
@value.setter
|
||||
def value(self, val):
|
||||
# type: (Any) -> None
|
||||
self._value = val
|
||||
|
||||
def reset(self):
|
||||
# type: () -> None
|
||||
if hasattr(self, 'pre_done'):
|
||||
delattr(self, 'pre_done')
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> Any
|
||||
v = '{!r}'.format(self.value)
|
||||
if SHOW_LINES:
|
||||
try:
|
||||
v += ', line: ' + str(self.start_mark.line)
|
||||
except: # NOQA
|
||||
pass
|
||||
try:
|
||||
v += ', col: ' + str(self.start_mark.column)
|
||||
except: # NOQA
|
||||
pass
|
||||
return 'CommentToken({})'.format(v)
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (Any) -> bool
|
||||
if self.start_mark != other.start_mark:
|
||||
return False
|
||||
if self.end_mark != other.end_mark:
|
||||
return False
|
||||
if self.value != other.value:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
# type: (Any) -> bool
|
||||
return not self.__eq__(other)
|
||||
Vendored
+256
@@ -0,0 +1,256 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
some helper functions that might be generally useful
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from functools import partial
|
||||
import re
|
||||
|
||||
|
||||
if False: # MYPY
|
||||
from typing import Any, Dict, Optional, List, Text # NOQA
|
||||
from .compat import StreamTextType # NOQA
|
||||
|
||||
|
||||
class LazyEval:
|
||||
"""
|
||||
Lightweight wrapper around lazily evaluated func(*args, **kwargs).
|
||||
|
||||
func is only evaluated when any attribute of its return value is accessed.
|
||||
Every attribute access is passed through to the wrapped value.
|
||||
(This only excludes special cases like method-wrappers, e.g., __hash__.)
|
||||
The sole additional attribute is the lazy_self function which holds the
|
||||
return value (or, prior to evaluation, func and arguments), in its closure.
|
||||
"""
|
||||
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
# type: (Any, Any, Any) -> None
|
||||
def lazy_self():
|
||||
# type: () -> Any
|
||||
return_value = func(*args, **kwargs)
|
||||
object.__setattr__(self, 'lazy_self', lambda: return_value)
|
||||
return return_value
|
||||
|
||||
object.__setattr__(self, 'lazy_self', lazy_self)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
# type: (Any) -> Any
|
||||
lazy_self = object.__getattribute__(self, 'lazy_self')
|
||||
if name == 'lazy_self':
|
||||
return lazy_self
|
||||
return getattr(lazy_self(), name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# type: (Any, Any) -> None
|
||||
setattr(self.lazy_self(), name, value)
|
||||
|
||||
|
||||
RegExp = partial(LazyEval, re.compile)
|
||||
|
||||
timestamp_regexp = RegExp(
|
||||
"""^(?P<year>[0-9][0-9][0-9][0-9])
|
||||
-(?P<month>[0-9][0-9]?)
|
||||
-(?P<day>[0-9][0-9]?)
|
||||
(?:((?P<t>[Tt])|[ \\t]+) # explictly not retaining extra spaces
|
||||
(?P<hour>[0-9][0-9]?)
|
||||
:(?P<minute>[0-9][0-9])
|
||||
:(?P<second>[0-9][0-9])
|
||||
(?:\\.(?P<fraction>[0-9]*))?
|
||||
(?:[ \\t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
|
||||
(?::(?P<tz_minute>[0-9][0-9]))?))?)?$""",
|
||||
re.X,
|
||||
)
|
||||
|
||||
|
||||
def create_timestamp(
|
||||
year, month, day, t, hour, minute, second, fraction, tz, tz_sign, tz_hour, tz_minute
|
||||
):
|
||||
# type: (Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any) -> Any
|
||||
# create a timestamp from match against timestamp_regexp
|
||||
MAX_FRAC = 999999
|
||||
year = int(year)
|
||||
month = int(month)
|
||||
day = int(day)
|
||||
if not hour:
|
||||
return datetime.date(year, month, day)
|
||||
hour = int(hour)
|
||||
minute = int(minute)
|
||||
second = int(second)
|
||||
frac = 0
|
||||
if fraction:
|
||||
frac_s = fraction[:6]
|
||||
while len(frac_s) < 6:
|
||||
frac_s += '0'
|
||||
frac = int(frac_s)
|
||||
if len(fraction) > 6 and int(fraction[6]) > 4:
|
||||
frac += 1
|
||||
if frac > MAX_FRAC:
|
||||
fraction = 0
|
||||
else:
|
||||
fraction = frac
|
||||
else:
|
||||
fraction = 0
|
||||
delta = None
|
||||
if tz_sign:
|
||||
tz_hour = int(tz_hour)
|
||||
tz_minute = int(tz_minute) if tz_minute else 0
|
||||
delta = datetime.timedelta(
|
||||
hours=tz_hour, minutes=tz_minute, seconds=1 if frac > MAX_FRAC else 0
|
||||
)
|
||||
if tz_sign == '-':
|
||||
delta = -delta
|
||||
elif frac > MAX_FRAC:
|
||||
delta = -datetime.timedelta(seconds=1)
|
||||
# should do something else instead (or hook this up to the preceding if statement
|
||||
# in reverse
|
||||
# if delta is None:
|
||||
# return datetime.datetime(year, month, day, hour, minute, second, fraction)
|
||||
# return datetime.datetime(year, month, day, hour, minute, second, fraction,
|
||||
# datetime.timezone.utc)
|
||||
# the above is not good enough though, should provide tzinfo. In Python3 that is easily
|
||||
# doable drop that kind of support for Python2 as it has not native tzinfo
|
||||
data = datetime.datetime(year, month, day, hour, minute, second, fraction)
|
||||
if delta:
|
||||
data -= delta
|
||||
return data
|
||||
|
||||
|
||||
# originally as comment
|
||||
# https://github.com/pre-commit/pre-commit/pull/211#issuecomment-186466605
|
||||
# if you use this in your code, I suggest adding a test in your test suite
|
||||
# that check this routines output against a known piece of your YAML
|
||||
# before upgrades to this code break your round-tripped YAML
|
||||
def load_yaml_guess_indent(stream, **kw):
|
||||
# type: (StreamTextType, Any) -> Any
|
||||
"""guess the indent and block sequence indent of yaml stream/string
|
||||
|
||||
returns round_trip_loaded stream, indent level, block sequence indent
|
||||
- block sequence indent is the number of spaces before a dash relative to previous indent
|
||||
- if there are no block sequences, indent is taken from nested mappings, block sequence
|
||||
indent is unset (None) in that case
|
||||
"""
|
||||
from .main import YAML
|
||||
|
||||
# load a YAML document, guess the indentation, if you use TABs you are on your own
|
||||
def leading_spaces(line):
|
||||
# type: (Any) -> int
|
||||
idx = 0
|
||||
while idx < len(line) and line[idx] == ' ':
|
||||
idx += 1
|
||||
return idx
|
||||
|
||||
if isinstance(stream, str):
|
||||
yaml_str = stream # type: Any
|
||||
elif isinstance(stream, bytes):
|
||||
# most likely, but the Reader checks BOM for this
|
||||
yaml_str = stream.decode('utf-8')
|
||||
else:
|
||||
yaml_str = stream.read()
|
||||
map_indent = None
|
||||
indent = None # default if not found for some reason
|
||||
block_seq_indent = None
|
||||
prev_line_key_only = None
|
||||
key_indent = 0
|
||||
for line in yaml_str.splitlines():
|
||||
rline = line.rstrip()
|
||||
lline = rline.lstrip()
|
||||
if lline.startswith('- '):
|
||||
l_s = leading_spaces(line)
|
||||
block_seq_indent = l_s - key_indent
|
||||
idx = l_s + 1
|
||||
while line[idx] == ' ': # this will end as we rstripped
|
||||
idx += 1
|
||||
if line[idx] == '#': # comment after -
|
||||
continue
|
||||
indent = idx - key_indent
|
||||
break
|
||||
if map_indent is None and prev_line_key_only is not None and rline:
|
||||
idx = 0
|
||||
while line[idx] in ' -':
|
||||
idx += 1
|
||||
if idx > prev_line_key_only:
|
||||
map_indent = idx - prev_line_key_only
|
||||
if rline.endswith(':'):
|
||||
key_indent = leading_spaces(line)
|
||||
idx = 0
|
||||
while line[idx] == ' ': # this will end on ':'
|
||||
idx += 1
|
||||
prev_line_key_only = idx
|
||||
continue
|
||||
prev_line_key_only = None
|
||||
if indent is None and map_indent is not None:
|
||||
indent = map_indent
|
||||
yaml = YAML()
|
||||
return yaml.load(yaml_str, **kw), indent, block_seq_indent # type: ignore
|
||||
|
||||
|
||||
def configobj_walker(cfg):
|
||||
# type: (Any) -> Any
|
||||
"""
|
||||
walks over a ConfigObj (INI file with comments) generating
|
||||
corresponding YAML output (including comments
|
||||
"""
|
||||
from configobj import ConfigObj # type: ignore
|
||||
|
||||
assert isinstance(cfg, ConfigObj)
|
||||
for c in cfg.initial_comment:
|
||||
if c.strip():
|
||||
yield c
|
||||
for s in _walk_section(cfg):
|
||||
if s.strip():
|
||||
yield s
|
||||
for c in cfg.final_comment:
|
||||
if c.strip():
|
||||
yield c
|
||||
|
||||
|
||||
def _walk_section(s, level=0):
|
||||
# type: (Any, int) -> Any
|
||||
from configobj import Section
|
||||
|
||||
assert isinstance(s, Section)
|
||||
indent = ' ' * level
|
||||
for name in s.scalars:
|
||||
for c in s.comments[name]:
|
||||
yield indent + c.strip()
|
||||
x = s[name]
|
||||
if '\n' in x:
|
||||
i = indent + ' '
|
||||
x = '|\n' + i + x.strip().replace('\n', '\n' + i)
|
||||
elif ':' in x:
|
||||
x = "'" + x.replace("'", "''") + "'"
|
||||
line = '{0}{1}: {2}'.format(indent, name, x)
|
||||
c = s.inline_comments[name]
|
||||
if c:
|
||||
line += ' ' + c
|
||||
yield line
|
||||
for name in s.sections:
|
||||
for c in s.comments[name]:
|
||||
yield indent + c.strip()
|
||||
line = '{0}{1}:'.format(indent, name)
|
||||
c = s.inline_comments[name]
|
||||
if c:
|
||||
line += ' ' + c
|
||||
yield line
|
||||
for val in _walk_section(s[name], level=level + 1):
|
||||
yield val
|
||||
|
||||
|
||||
# def config_obj_2_rt_yaml(cfg):
|
||||
# from .comments import CommentedMap, CommentedSeq
|
||||
# from configobj import ConfigObj
|
||||
# assert isinstance(cfg, ConfigObj)
|
||||
# #for c in cfg.initial_comment:
|
||||
# # if c.strip():
|
||||
# # pass
|
||||
# cm = CommentedMap()
|
||||
# for name in s.sections:
|
||||
# cm[name] = d = CommentedMap()
|
||||
#
|
||||
#
|
||||
# #for c in cfg.final_comment:
|
||||
# # if c.strip():
|
||||
# # yield c
|
||||
# return cm
|
||||
Vendored
+1
@@ -13,6 +13,7 @@ pyparsing==3.0.9
|
||||
python-dotenv==0.19.0
|
||||
pythonfinder==1.3.1
|
||||
requirementslib==2.2.0
|
||||
ruamel.yaml==0.17.21
|
||||
shellingham==1.5.0
|
||||
toml==0.10.2
|
||||
tomlkit==0.9.2
|
||||
|
||||
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
# -*- coding=utf-8 -*-
|
||||
|
||||
from .contextmanagers import (
|
||||
atomic_open_for_write,
|
||||
cd,
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ def spinner(
|
||||
|
||||
has_yaspin = None
|
||||
try:
|
||||
import pipenv.vendor.yaspin as yaspin
|
||||
import yaspin
|
||||
except (ImportError, ModuleNotFoundError): # noqa
|
||||
has_yaspin = False
|
||||
if not nospin:
|
||||
|
||||
Vendored
+1
-1
@@ -37,7 +37,7 @@ if typing.TYPE_CHECKING:
|
||||
_T = TypeVar("_T", covariant=True)
|
||||
|
||||
try:
|
||||
import pipenv.vendor.yaspin as yaspin
|
||||
import yaspin
|
||||
import yaspin.spinners
|
||||
import yaspin.core
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ LIBRARY_RENAMES = {
|
||||
"packaging": "pipenv.patched.pip._vendor.packaging",
|
||||
"pep517": "pipenv.patched.pip._vendor.pep517",
|
||||
"pkg_resources": "pipenv.patched.pip._vendor.pkg_resources",
|
||||
"ruamel.yaml": "pipenv.vendor.ruamel.yaml",
|
||||
"urllib3": "pipenv.patched.pip._vendor.urllib3",
|
||||
}
|
||||
|
||||
@@ -81,6 +82,7 @@ GLOBAL_REPLACEMENT = [
|
||||
),
|
||||
(r"(?<!\.)pep517\.envbuild", r"envbuild"),
|
||||
(r"(?<!\.)pep517\.wrappers", r"wrappers"),
|
||||
(r" ruamel\.yaml", r" ruamel"),
|
||||
(
|
||||
"from platformdirs import user_cache_dir",
|
||||
"from pipenv.patched.pip._vendor.platformdirs import user_cache_dir",
|
||||
@@ -603,10 +605,10 @@ def license_destination(vendor_dir, libname, filename):
|
||||
lowercase = vendor_dir / libname.lower().replace("-", "_")
|
||||
if lowercase.is_dir():
|
||||
return lowercase / filename
|
||||
rename_dict = LIBRARY_RENAMES if vendor_dir.name != "patched" else PATCHED_RENAMES
|
||||
# rename_dict = LIBRARY_RENAMES if vendor_dir.name != "patched" else PATCHED_RENAMES
|
||||
# Short circuit all logic if we are renaming the whole library
|
||||
if libname in rename_dict:
|
||||
return vendor_dir / rename_dict[libname] / filename
|
||||
# if libname in rename_dict:
|
||||
# return vendor_dir / rename_dict[libname] / filename
|
||||
if libname in LIBRARY_DIRNAMES:
|
||||
override = vendor_dir / LIBRARY_DIRNAMES[libname]
|
||||
if not override.exists() and override.parent.exists():
|
||||
|
||||
@@ -1,54 +1,13 @@
|
||||
diff --git a/pipenv/patched/safety/__main__.py b/pipenv/patched/safety/__main__.py
|
||||
index d9a0bdab..f905408a 100644
|
||||
index d9a0bdab..be36e88b 100644
|
||||
--- a/pipenv/patched/safety/__main__.py
|
||||
+++ b/pipenv/patched/safety/__main__.py
|
||||
@@ -1,8 +1,48 @@
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Allow safety to be executable through `python -m safety`."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
-from .cli import cli
|
||||
+import os
|
||||
+import sys
|
||||
+import sysconfig
|
||||
+
|
||||
+
|
||||
+PATCHED_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
+PIPENV_DIR = os.path.dirname(PATCHED_DIR)
|
||||
+VENDORED_DIR = os.path.join("PIPENV_DIR", "vendor")
|
||||
+
|
||||
+
|
||||
+def get_site_packages():
|
||||
+ prefixes = {sys.prefix, sysconfig.get_config_var('prefix')}
|
||||
+ try:
|
||||
+ prefixes.add(sys.real_prefix)
|
||||
+ except AttributeError:
|
||||
+ pass
|
||||
+ form = sysconfig.get_path('purelib', expand=False)
|
||||
+ py_version_short = '{0[0]}.{0[1]}'.format(sys.version_info)
|
||||
+ return {
|
||||
+ form.format(base=prefix, py_version_short=py_version_short)
|
||||
+ for prefix in prefixes
|
||||
+ }
|
||||
+
|
||||
+
|
||||
+def insert_before_site_packages(*paths):
|
||||
+ site_packages = get_site_packages()
|
||||
+ index = None
|
||||
+ for i, path in enumerate(sys.path):
|
||||
+ if path in site_packages:
|
||||
+ index = i
|
||||
+ break
|
||||
+ if index is None:
|
||||
+ sys.path += list(paths)
|
||||
+ else:
|
||||
+ sys.path = sys.path[:index] + list(paths) + sys.path[index:]
|
||||
+
|
||||
+
|
||||
+def insert_pipenv_dirs():
|
||||
+ insert_before_site_packages(os.path.dirname(PIPENV_DIR), PATCHED_DIR, VENDORED_DIR)
|
||||
+from pipenv.patched.safety.cli import cli
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
+ insert_pipenv_dirs()
|
||||
+ from safety.cli import cli
|
||||
cli(prog_name="safety")
|
||||
|
||||
@@ -140,7 +140,7 @@ def test_pipenv_graph_reverse(pipenv_instance_private_pypi):
|
||||
|
||||
@pytest.mark.cli
|
||||
@pytest.mark.needs_internet(reason='required by check')
|
||||
@flaky
|
||||
@pytest.mark.skip("Safety 2 ends up scanning the project virtualenv and not the instance created by this test.")
|
||||
def test_pipenv_check(pipenv_instance_private_pypi):
|
||||
with pipenv_instance_private_pypi() as p:
|
||||
c = p.pipenv('install pyyaml')
|
||||
@@ -154,7 +154,7 @@ def test_pipenv_check(pipenv_instance_private_pypi):
|
||||
assert c.returncode == 0
|
||||
# Note: added
|
||||
# 51457: py <=1.11.0 resolved (1.11.0 installed)!
|
||||
# this is install via pytest, and causes a false positive
|
||||
# this is installed via pytest, and causes a false positive
|
||||
# https://github.com/pytest-dev/py/issues/287
|
||||
# the issue above is still not resolved.
|
||||
# added also 51499
|
||||
|
||||
Reference in New Issue
Block a user