Merge branch 'master' into feature/keep-outdated-peep

Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
Dan Ryan
2019-03-07 01:03:49 -05:00
84 changed files with 4690 additions and 2032 deletions
+2 -3
View File
@@ -10,9 +10,8 @@ jobs:
maxParallel: 4
matrix:
${{ if eq(parameters.vmImage, 'vs2017-win2016') }}:
# TODO remove once vs2017-win2016 has Python 3.7
Python37:
python.version: '>= 3.7.0-b2'
python.version: '>= 3.7.2'
python.architecture: x64
${{ if ne(parameters.vmImage, 'vs2017-win2016' )}}:
Python37:
@@ -33,7 +32,7 @@ jobs:
pip install certifi
export GIT_SSL_CAINFO=$(python -m certifi)
export LANG=C.UTF-8
python -m pip install --upgrade invoke requests parver bs4 vistir towncrier
python -m pip install --upgrade invoke requests parver bs4 vistir towncrier pip setuptools wheel --upgrade-strategy=eager
python -m invoke vendoring.update
- template: ./run-manifest-check.yml
+1
View File
@@ -0,0 +1 @@
Fixed a bug which caused editable package resolution to sometimes fail with an unhelpful setuptools-related error message.
+1
View File
@@ -0,0 +1 @@
Dependency resolution now writes hashes for local and remote files to the lockfile.
+1
View File
@@ -0,0 +1 @@
Fixed a bug which prevented ``pipenv graph`` from correctly showing all dependencies when running from within ``pipenv shell``.
+3
View File
@@ -0,0 +1,3 @@
Fixed a bug which caused failures in warning reporting when running pipenv inside a virtualenv under some circumstances.
- Fixed a bug with package discovery when running ``pipenv clean``.
+5
View File
@@ -0,0 +1,5 @@
Added full support for resolution of all dependency types including direct URLs, zip archives, tarballs, etc.
- Improved error handling and formatting.
- Introduced improved cross platform stream wrappers for better ``stdout`` and ``stderr`` consistency.
+28
View File
@@ -0,0 +1,28 @@
Updated vendored dependencies:
- **attrs**: ``18.2.0`` => ``19.1.0``
- **certifi**: ``2018.10.15`` => ``2018.11.29``
- **cached_property**: ``1.4.3`` => ``1.5.1``
- **colorama**: ``0.3.9`` => ``0.4.1``
- **idna**: ``2.7`` => ``2.8``
- **markupsafe**: ``1.0`` => ``1.1.1``
- **orderedmultidict**: ``(new)`` => ``1.0``
- **packaging**: ``18.0`` => ``19.0``
- **parse**: ``1.9.0`` => ``1.11.1``
- **pathlib2**: ``2.3.2`` => ``2.3.3``
- **pep517**: ``(new)`` => ``0.5.0``
- **pipdeptree**: ``0.13.0`` => ``0.13.2``
- **pyparsing**: ``2.2.2`` => ``2.3.1``
- **python-dotenv**: ``0.9.1`` => ``0.10.1``
- **pythonfinder**: ``1.1.10`` => ``1.2.0``
- **pytoml**: ``(new)`` => ``0.1.20``
- **requests**: ``2.20.1`` => ``2.21.0``
- **requirementslib**: ``1.3.3`` => ``1.4.2``
- **shellingham**: ``1.2.7`` => ``1.2.8``
- **six**: ``1.11.0`` => ``1.12.0``
- **tomlkit**: ``0.5.2`` => ``0.5.3``
- **urllib3**: ``1.24`` => ``1.24.1``
- **vistir**: ``0.3.0`` => ``0.3.1``
- **yaspin**: ``0.14.0`` => ``0.14.1``
- Removed vendored dependency **cursor**.
+1
View File
@@ -0,0 +1 @@
Pipenv will now successfully recursively lock VCS sub-dependencies.
+1
View File
@@ -0,0 +1 @@
Pipenv will now discover and resolve the intrinsic dependencies of **all** VCS dependencies, whether they are editable or not, to prevent resolution conflicts.
+1
View File
@@ -0,0 +1 @@
Fixed a keyerror which could occur when locking VCS dependencies in some cases.
+1 -1
View File
@@ -1 +1 @@
Fix a bug that ``ValidationError`` is thrown when some fields are missing in source section.
Fixed a bug that ``ValidationError`` is thrown when some fields are missing in source section.
+1 -1
View File
@@ -1 +1 @@
Fix the wrong order of old and new hashes in message.
Fixed the wrong order of old and new hashes in message.
+1 -1
View File
@@ -1 +1 @@
Update the index names in lock file when source name in Pipfile is changed.
Updated the index names in lock file when source name in Pipfile is changed.
+1 -7
View File
@@ -12,8 +12,6 @@ import click_completion
import crayons
import delegator
from click_didyoumean import DYMCommandCollection
from ..__version__ import __version__
from .options import (
CONTEXT_SETTINGS, PipenvGroup, code_option, common_options, deploy_option,
@@ -400,8 +398,7 @@ def shell(
@pass_state
def run(state, command, args):
"""Spawns a command installed into the virtualenv."""
from ..core import do_run, warn_in_virtualenv
warn_in_virtualenv()
from ..core import do_run
do_run(
command=command, args=args, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror
)
@@ -634,8 +631,5 @@ def clean(ctx, state, dry_run=False, bare=False, user=False):
system=state.system)
# Only invoke the "did you mean" when an argument wasn't passed (it breaks those).
if "-" not in "".join(sys.argv) and len(sys.argv) > 1:
cli = DYMCommandCollection(sources=[cli])
if __name__ == "__main__":
cli()
+2 -1
View File
@@ -8,6 +8,7 @@ import click.types
from click import (
BadParameter, Group, Option, argument, echo, make_pass_decorator, option
)
from click_didyoumean import DYMMixin
from .. import environments
from ..utils import is_valid_url
@@ -19,7 +20,7 @@ CONTEXT_SETTINGS = {
}
class PipenvGroup(Group):
class PipenvGroup(DYMMixin, Group):
"""Custom Group class provides formatted main help"""
def get_help_option(self, ctx):
+3 -7
View File
@@ -1281,7 +1281,7 @@ def pip_install(
use_pep517=True
):
from pipenv.patched.notpip._internal import logger as piplogger
from .utils import Mapping
from .vendor.vistir.compat import Mapping
from .vendor.urllib3.util import parse_url
src = []
write_to_tmpfile = False
@@ -1432,20 +1432,16 @@ def pip_install(
possible_hashes = install_reqs[:]
editable_opt, req = req.split(" ", 1)
install_reqs = [editable_opt, req] + install_reqs
# hashes must be passed via a file
ignore_hashes = True
# if possible_hashes and not any(
# item.startswith("--hash") for item in possible_hashes.split()
# ):
# ignore_hashes = True
elif r:
install_reqs = ["-r", r]
with open(r) as f:
if "--hash" not in f.read():
ignore_hashes = True
else:
# hashes need to be passed via a file
ignore_hashes = True
ignore_hashes = True if not requirement.hashes else ignore_hashes
install_reqs = requirement.as_line(as_list=True, include_hashes=not ignore_hashes)
if not requirement.markers:
install_reqs = [escape_cmd(r) for r in install_reqs]
+2 -1
View File
@@ -188,7 +188,8 @@ class Environment(object):
@cached_property
def sys_path(self):
"""The system path inside the environment
"""
The system path inside the environment
:return: The :data:`sys.path` from the environment
:rtype: list
+9 -2
View File
@@ -179,7 +179,8 @@ class SystemUsageError(PipenvOptionsError):
crayons.red("Warning", bold=True)
),
]
message = crayons.blue("See also: {0}".format(crayons.white("-deploy flag.")))
if message is None:
message = crayons.blue("See also: {0}".format(crayons.white("--deploy flag.")))
super(SystemUsageError, self).__init__(option_name, message=message, ctx=ctx, extra=extra, **kwargs)
@@ -248,8 +249,14 @@ class UninstallError(PipenvException):
class InstallError(PipenvException):
def __init__(self, package, **kwargs):
message = "{0} {1}".format(
package_message = ""
if package is not None:
package_message = crayons.normal("Couldn't install package {0}\n".format(
crayons.white(package, bold=True)
))
message = "{0} {1} {2}".format(
crayons.red("ERROR:", bold=True),
package_message,
crayons.yellow("Package installation failed...")
)
extra = kwargs.pop("extra", [])
+1 -1
View File
@@ -2,4 +2,4 @@
__author__ = """pyup.io"""
__email__ = 'support@pyup.io'
__version__ = '1.8.4'
__version__ = '1.8.5'
+68 -26
View File
@@ -6,17 +6,13 @@ from safety import __version__
from safety import safety
from safety.formatter import report
import itertools
from safety.util import read_requirements
from safety.util import read_requirements, read_vulnerabilities
from safety.errors import DatabaseFetchError, DatabaseFileNotFoundError, InvalidKeyError
try:
# pip 9
from pipenv.patched.notpip import get_installed_distributions
from json.decoder import JSONDecodeError
except ImportError:
# pip 10
from pipenv.patched.notpip._internal.utils.misc import get_installed_distributions
JSONDecodeError = ValueError
@click.group()
@click.version_option(version=__version__)
@@ -46,10 +42,17 @@ def cli():
help="Read input from one (or multiple) requirement files. Default: empty")
@click.option("ignore", "--ignore", "-i", multiple=True, type=str, default=[],
help="Ignore one (or multiple) vulnerabilities by ID. Default: empty")
def check(key, db, json, full_report, bare, stdin, files, cache, ignore):
@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',
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")
click.secho("Can't read from --stdin and --file at the same time, exiting", fg="red", file=sys.stderr)
sys.exit(-1)
if files:
@@ -57,33 +60,72 @@ def check(key, db, json, full_report, bare, stdin, files, cache, ignore):
elif stdin:
packages = list(read_requirements(sys.stdin))
else:
packages = get_installed_distributions()
import pkg_resources
packages = [
d for d in pkg_resources.working_set
if d.key not in {"python", "wsgiref", "argparse"}
]
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)
try:
vulns = safety.check(packages=packages, key=key, db_mirror=db, cached=cache, ignore_ids=ignore)
click.secho(report(
vulns=vulns,
full=full_report,
json_report=json,
bare_report=bare,
checked_packages=len(packages),
db=db,
key=key
)
)
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)
if output:
with open(output, 'w+') as output_file:
output_file.write(output_report)
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")
fg="red",
file=sys.stderr)
sys.exit(-1)
except DatabaseFileNotFoundError:
click.secho("Unable to load vulnerability database from {db}".format(db=db), fg="red")
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")
click.secho("Unable to load vulnerability database", fg="red", file=sys.stderr)
sys.exit(-1)
@cli.command()
@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("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)
try:
input_vulns = read_vulnerabilities(file)
except JSONDecodeError:
click.secho("Not a valid JSON file", fg="red")
sys.exit(-1)
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)
if __name__ == "__main__":
cli()
+5 -3
View File
@@ -3,6 +3,7 @@ import platform
import sys
import json
import os
import textwrap
# python 2.7 compat
try:
@@ -110,9 +111,10 @@ class SheetReport(object):
descr = get_advisory(vuln)
for chunk in [descr[i:i + 76] for i in range(0, len(descr), 76)]:
for line in chunk.splitlines():
for pn, paragraph in enumerate(descr.replace('\r', '').split('\n\n')):
if pn:
table.append("{:76}".format(''))
for line in textwrap.wrap(paragraph, width=76):
try:
table.append("{:76}".format(line.encode('utf-8')))
except TypeError:
+23 -8
View File
@@ -9,6 +9,7 @@ import json
import time
import errno
class Vulnerability(namedtuple("Vulnerability",
["name", "spec", "version", "advisory", "vuln_id"])):
pass
@@ -64,7 +65,7 @@ def write_to_cache(db_name, data):
f.write(json.dumps(cache))
def fetch_database_url(mirror, db_name, key, cached):
def fetch_database_url(mirror, db_name, key, cached, proxy):
headers = {}
if key:
@@ -74,9 +75,8 @@ def fetch_database_url(mirror, db_name, key, cached):
cached_data = get_from_cache(db_name=db_name)
if cached_data:
return cached_data
url = mirror + db_name
r = requests.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers)
r = requests.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy)
if r.status_code == 200:
data = r.json()
if cached:
@@ -94,7 +94,7 @@ def fetch_database_file(path, db_name):
return json.loads(f.read())
def fetch_database(full=False, key=False, db=False, cached=False):
def fetch_database(full=False, key=False, db=False, cached=False, proxy={}):
if db:
mirrors = [db]
@@ -105,7 +105,7 @@ def fetch_database(full=False, key=False, db=False, cached=False):
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)
data = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy)
else:
data = fetch_database_file(mirror, db_name=db_name)
if data:
@@ -120,10 +120,9 @@ def get_vulnerabilities(pkg, spec, db):
yield entry
def check(packages, key, db_mirror, cached, ignore_ids):
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)
db = fetch_database(key=key, db=db_mirror, cached=cached, proxy=proxy)
db_full = None
vulnerable_packages = frozenset(db.keys())
vulnerable = []
@@ -152,3 +151,19 @@ def check(packages, key, db_mirror, cached, ignore_ids):
)
)
return vulnerable
def review(vulnerabilities):
vulnerable = []
for vuln in vulnerabilities:
current_vuln = {
"name": vuln[0],
"spec": vuln[1],
"version": vuln[2],
"advisory": vuln[3],
"vuln_id": vuln[4],
}
vulnerable.append(
Vulnerability(**current_vuln)
)
return vulnerable
+8 -1
View File
@@ -1,11 +1,17 @@
from dparse.parser import setuptools_parse_requirements_backport as _parse_requirements
from collections import namedtuple
import click
import sys
import json
import os
Package = namedtuple("Package", ["key", "version"])
RequirementFile = namedtuple("RequirementFile", ["path"])
def read_vulnerabilities(fh):
return json.load(fh)
def iter_lines(fh, lineno=0):
for line in fh.readlines()[lineno:]:
yield line
@@ -85,7 +91,8 @@ def read_requirements(fh, resolve=False):
"Warning: unpinned requirement '{req}' found in {fname}, "
"unable to check.".format(req=req.name,
fname=fname),
fg="yellow"
fg="yellow",
file=sys.stderr
)
except ValueError:
continue
+1
View File
@@ -539,6 +539,7 @@ class Project(object):
def build_requires(self):
return self._build_system.get("requires", ["setuptools>=40.8.0", "wheel"])
@property
def build_backend(self):
return self._build_system.get("build-backend", get_default_pyproject_backend())
+18 -5
View File
@@ -330,6 +330,7 @@ class Resolver(object):
):
# type: (...) -> Tuple[Requirement, Dict[str, str], Dict[str, str]]
from .vendor.requirementslib.models.requirements import Requirement
from .exceptions import ResolutionFailure
if index_lookup is None:
index_lookup = {}
if markers_lookup is None:
@@ -343,7 +344,10 @@ class Resolver(object):
url = indexes[0]
line = " ".join(remainder)
req = None # type: Requirement
req = Requirement.from_line(line)
try:
req = Requirement.from_line(line)
except ValueError:
raise ResolutionFailure("Failed to resolve requirement from line: {0!s}".format(line))
if url:
try:
index_lookup[req.normalized_name] = project.get_source(
@@ -397,7 +401,14 @@ class Resolver(object):
continue
line = _requirement_to_str_lowercase_name(r)
new_req, _, _ = cls.parse_line(line)
new_constraints, new_lock = cls.get_deps_from_req(new_req)
if r.marker and not r.marker.evaluate():
new_constraints = {}
_, new_entry = req.pipfile_entry
new_lock = {
pep423_name(new_req.normalized_name): new_entry
}
else:
new_constraints, new_lock = cls.get_deps_from_req(new_req)
locked_deps.update(new_lock)
constraints |= new_constraints
else:
@@ -767,14 +778,15 @@ def actually_resolve_deps(
@contextlib.contextmanager
def create_spinner(text, nospin=None, spinner_name=None):
import vistir.spin
from .vendor.vistir import spin
from .vendor.vistir.misc import fs_str
if not spinner_name:
spinner_name = environments.PIPENV_SPINNER
if nospin is None:
nospin = environments.PIPENV_NOSPIN
with vistir.spin.create_spinner(
with spin.create_spinner(
spinner_name=spinner_name,
start_text=vistir.compat.fs_str(text),
start_text=fs_str(text),
nospin=nospin, write_to_stdout=False
) as sp:
yield sp
@@ -947,6 +959,7 @@ def venv_resolve_deps(
cmd.append("--dev")
with temp_environ():
os.environ.update({fs_str(k): fs_str(val) for k, val in os.environ.items()})
os.environ["PIPENV_PACKAGES"] = str("\n".join(constraints))
if pypi_mirror:
os.environ["PIPENV_PYPI_MIRROR"] = str(pypi_mirror)
os.environ["PIPENV_VERBOSITY"] = str(environments.PIPENV_VERBOSITY)
+1 -1
View File
@@ -18,7 +18,7 @@ from ._make import (
)
__version__ = "18.2.0"
__version__ = "19.1.0"
__title__ = "attrs"
__description__ = "Classes Without Boilerplate"
+15 -12
View File
@@ -23,9 +23,9 @@ from . import validators as validators
_T = TypeVar("_T")
_C = TypeVar("_C", bound=type)
_ValidatorType = Callable[[Any, Attribute, _T], Any]
_ValidatorType = Callable[[Any, Attribute[_T], _T], Any]
_ConverterType = Callable[[Any], _T]
_FilterType = Callable[[Attribute, Any], bool]
_FilterType = Callable[[Attribute[_T], _T], bool]
# FIXME: in reality, if multiple validators are passed they must be in a list or tuple,
# but those are invariant and so would prevent subtypes of _ValidatorType from working
# when passed in a list or tuple.
@@ -57,10 +57,10 @@ class Attribute(Generic[_T]):
metadata: Dict[Any, Any]
type: Optional[Type[_T]]
kw_only: bool
def __lt__(self, x: Attribute) -> bool: ...
def __le__(self, x: Attribute) -> bool: ...
def __gt__(self, x: Attribute) -> bool: ...
def __ge__(self, x: Attribute) -> bool: ...
def __lt__(self, x: Attribute[_T]) -> bool: ...
def __le__(self, x: Attribute[_T]) -> bool: ...
def __gt__(self, x: Attribute[_T]) -> bool: ...
def __ge__(self, x: Attribute[_T]) -> bool: ...
# NOTE: We had several choices for the annotation to use for type arg:
# 1) Type[_T]
@@ -167,6 +167,7 @@ def attrs(
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
) -> _C: ...
@overload
def attrs(
@@ -184,14 +185,15 @@ def attrs(
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
) -> Callable[[_C], _C]: ...
# TODO: add support for returning NamedTuple from the mypy plugin
class _Fields(Tuple[Attribute, ...]):
def __getattr__(self, name: str) -> Attribute: ...
class _Fields(Tuple[Attribute[Any], ...]):
def __getattr__(self, name: str) -> Attribute[Any]: ...
def fields(cls: type) -> _Fields: ...
def fields_dict(cls: type) -> Dict[str, Attribute]: ...
def fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ...
def validate(inst: Any) -> None: ...
# TODO: add support for returning a proper attrs class from the mypy plugin
@@ -212,6 +214,7 @@ def make_class(
auto_attribs: bool = ...,
kw_only: bool = ...,
cache_hash: bool = ...,
auto_exc: bool = ...,
) -> type: ...
# _funcs --
@@ -223,7 +226,7 @@ def make_class(
def asdict(
inst: Any,
recurse: bool = ...,
filter: Optional[_FilterType] = ...,
filter: Optional[_FilterType[Any]] = ...,
dict_factory: Type[Mapping[Any, Any]] = ...,
retain_collection_types: bool = ...,
) -> Dict[str, Any]: ...
@@ -232,8 +235,8 @@ def asdict(
def astuple(
inst: Any,
recurse: bool = ...,
filter: Optional[_FilterType] = ...,
tuple_factory: Type[Sequence] = ...,
filter: Optional[_FilterType[Any]] = ...,
tuple_factory: Type[Sequence[Any]] = ...,
retain_collection_types: bool = ...,
) -> Tuple[Any, ...]: ...
def has(cls: type) -> bool: ...
+21 -25
View File
@@ -20,6 +20,7 @@ else:
if PY2:
from UserDict import IterableUserDict
from collections import Mapping, Sequence # noqa
# We 'bundle' isclass instead of using inspect as importing inspect is
# fairly expensive (order of 10-15 ms for a modern machine in 2016)
@@ -89,8 +90,27 @@ if PY2:
res.data.update(d) # We blocked update, so we have to do it like this.
return res
def just_warn(*args, **kw): # pragma: nocover
"""
We only warn on Python 3 because we are not aware of any concrete
consequences of not setting the cell on Python 2.
"""
else:
else: # Python 3 and later.
from collections.abc import Mapping, Sequence # noqa
def just_warn(*args, **kw):
"""
We only warn on Python 3 because we are not aware of any concrete
consequences of not setting the cell on Python 2.
"""
warnings.warn(
"Missing ctypes. Some features like bare super() or accessing "
"__class__ will not work with slotted classes.",
RuntimeWarning,
stacklevel=2,
)
def isclass(klass):
return isinstance(klass, type)
@@ -113,30 +133,6 @@ def import_ctypes():
return ctypes
if not PY2:
def just_warn(*args, **kw):
"""
We only warn on Python 3 because we are not aware of any concrete
consequences of not setting the cell on Python 2.
"""
warnings.warn(
"Missing ctypes. Some features like bare super() or accessing "
"__class__ will not work with slots classes.",
RuntimeWarning,
stacklevel=2,
)
else:
def just_warn(*args, **kw): # pragma: nocover
"""
We only warn on Python 3 because we are not aware of any concrete
consequences of not setting the cell on Python 2.
"""
def make_set_closure_cell():
"""
Moved into a function for testability.
+79 -27
View File
@@ -409,12 +409,11 @@ def _transform_attrs(cls, these, auto_attribs, kw_only):
a.kw_only is False
):
had_default = True
if was_kw_only is True and a.kw_only is False:
if was_kw_only is True and a.kw_only is False and a.init is True:
raise ValueError(
"Non keyword-only attributes are not allowed after a "
"keyword-only attribute. Attribute in question: {a!r}".format(
a=a
)
"keyword-only attribute (unless they are init=False). "
"Attribute in question: {a!r}".format(a=a)
)
if was_kw_only is False and a.init is True and a.kw_only is True:
was_kw_only = True
@@ -454,6 +453,7 @@ class _ClassBuilder(object):
"_has_post_init",
"_delete_attribs",
"_base_attr_map",
"_is_exc",
)
def __init__(
@@ -466,6 +466,7 @@ class _ClassBuilder(object):
auto_attribs,
kw_only,
cache_hash,
is_exc,
):
attrs, base_attrs, base_map = _transform_attrs(
cls, these, auto_attribs, kw_only
@@ -483,6 +484,7 @@ class _ClassBuilder(object):
self._cache_hash = cache_hash
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these)
self._is_exc = is_exc
self._cls_dict["__attrs_attrs__"] = self._attrs
@@ -530,6 +532,26 @@ class _ClassBuilder(object):
for name, value in self._cls_dict.items():
setattr(cls, name, value)
# Attach __setstate__. This is necessary to clear the hash code
# cache on deserialization. See issue
# https://github.com/python-attrs/attrs/issues/482 .
# Note that this code only handles setstate for dict classes.
# For slotted classes, see similar code in _create_slots_class .
if self._cache_hash:
existing_set_state_method = getattr(cls, "__setstate__", None)
if existing_set_state_method:
raise NotImplementedError(
"Currently you cannot use hash caching if "
"you specify your own __setstate__ method."
"See https://github.com/python-attrs/attrs/issues/494 ."
)
def cache_hash_set_state(chss_self, _):
# clear hash code cache
setattr(chss_self, _hash_cache_field, None)
setattr(cls, "__setstate__", cache_hash_set_state)
return cls
def _create_slots_class(self):
@@ -582,6 +604,8 @@ class _ClassBuilder(object):
"""
return tuple(getattr(self, name) for name in state_attr_names)
hash_caching_enabled = self._cache_hash
def slots_setstate(self, state):
"""
Automatically created by attrs.
@@ -589,6 +613,13 @@ class _ClassBuilder(object):
__bound_setattr = _obj_setattr.__get__(self, Attribute)
for name, value in zip(state_attr_names, state):
__bound_setattr(name, value)
# Clearing the hash code cache on deserialization is needed
# because hash codes can change from run to run. See issue
# https://github.com/python-attrs/attrs/issues/482 .
# Note that this code only handles setstate for slotted classes.
# For dict classes, see similar code in _patch_original_class .
if hash_caching_enabled:
__bound_setattr(_hash_cache_field, None)
# slots and frozen require __getstate__/__setstate__ to work
cd["__getstate__"] = slots_getstate
@@ -660,6 +691,7 @@ class _ClassBuilder(object):
self._slots,
self._cache_hash,
self._base_attr_map,
self._is_exc,
)
)
@@ -710,6 +742,7 @@ def attrs(
auto_attribs=False,
kw_only=False,
cache_hash=False,
auto_exc=False,
):
r"""
A class decorator that adds `dunder
@@ -815,10 +848,23 @@ def attrs(
:param bool cache_hash: Ensure that the object's hash code is computed
only once and stored on the object. If this is set to ``True``,
hashing must be either explicitly or implicitly enabled for this
class. If the hash code is cached, then no attributes of this
class which participate in hash code computation may be mutated
after object creation.
class. If the hash code is cached, avoid any reassignments of
fields involved in hash code computation or mutations of the objects
those fields point to after object creation. If such changes occur,
the behavior of the object's hash code is undefined.
:param bool auto_exc: If the class subclasses :class:`BaseException`
(which implicitly includes any subclass of any exception), the
following happens to behave like a well-behaved Python exceptions
class:
- the values for *cmp* and *hash* are ignored and the instances compare
and hash by the instance's ids (N.B. ``attrs`` will *not* remove
existing implementations of ``__hash__`` or the equality methods. It
just won't add own ones.),
- all attributes that are either passed into ``__init__`` or have a
default value are additionally available as a tuple in the ``args``
attribute,
- the value of *str* is ignored leaving ``__str__`` to base classes.
.. versionadded:: 16.0.0 *slots*
.. versionadded:: 16.1.0 *frozen*
@@ -838,12 +884,16 @@ def attrs(
to each other.
.. versionadded:: 18.2.0 *kw_only*
.. versionadded:: 18.2.0 *cache_hash*
.. versionadded:: 19.1.0 *auto_exc*
"""
def wrap(cls):
if getattr(cls, "__class__", None) is None:
raise TypeError("attrs only works with new-style classes.")
is_exc = auto_exc is True and issubclass(cls, BaseException)
builder = _ClassBuilder(
cls,
these,
@@ -853,13 +903,14 @@ def attrs(
auto_attribs,
kw_only,
cache_hash,
is_exc,
)
if repr is True:
builder.add_repr(repr_ns)
if str is True:
builder.add_str()
if cmp is True:
if cmp is True and not is_exc:
builder.add_cmp()
if hash is not True and hash is not False and hash is not None:
@@ -874,7 +925,11 @@ def attrs(
" hashing must be either explicitly or implicitly "
"enabled."
)
elif hash is True or (hash is None and cmp is True and frozen is True):
elif (
hash is True
or (hash is None and cmp is True and frozen is True)
and is_exc is False
):
builder.add_hash()
else:
if cache_hash:
@@ -1213,7 +1268,9 @@ def _add_repr(cls, ns=None, attrs=None):
return cls
def _make_init(attrs, post_init, frozen, slots, cache_hash, base_attr_map):
def _make_init(
attrs, post_init, frozen, slots, cache_hash, base_attr_map, is_exc
):
attrs = [a for a in attrs if a.init or a.default is not NOTHING]
# We cache the generated init methods for the same kinds of attributes.
@@ -1222,16 +1279,18 @@ def _make_init(attrs, post_init, frozen, slots, cache_hash, base_attr_map):
unique_filename = "<attrs generated init {0}>".format(sha1.hexdigest())
script, globs, annotations = _attrs_to_init_script(
attrs, frozen, slots, post_init, cache_hash, base_attr_map
attrs, frozen, slots, post_init, cache_hash, base_attr_map, is_exc
)
locs = {}
bytecode = compile(script, unique_filename, "exec")
attr_dict = dict((a.name, a) for a in attrs)
globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict})
if frozen is True:
# Save the lookup overhead in __init__ if we need to circumvent
# immutability.
globs["_cached_setattr"] = _obj_setattr
eval(bytecode, globs, locs)
# In order of debuggers like PDB being able to step through the code,
@@ -1245,24 +1304,10 @@ def _make_init(attrs, post_init, frozen, slots, cache_hash, base_attr_map):
__init__ = locs["__init__"]
__init__.__annotations__ = annotations
return __init__
def _add_init(cls, frozen):
"""
Add a __init__ method to *cls*. If *frozen* is True, make it immutable.
"""
cls.__init__ = _make_init(
cls.__attrs_attrs__,
getattr(cls, "__attrs_post_init__", False),
frozen,
_is_slot_cls(cls),
cache_hash=False,
base_attr_map={},
)
return cls
def fields(cls):
"""
Return the tuple of ``attrs`` attributes for a class.
@@ -1348,7 +1393,7 @@ def _is_slot_attr(a_name, base_attr_map):
def _attrs_to_init_script(
attrs, frozen, slots, post_init, cache_hash, base_attr_map
attrs, frozen, slots, post_init, cache_hash, base_attr_map, is_exc
):
"""
Return a script of an initializer for *attrs* and a dict of globals.
@@ -1597,6 +1642,13 @@ def _attrs_to_init_script(
init_hash_cache = "self.%s = %s"
lines.append(init_hash_cache % (_hash_cache_field, "None"))
# For exceptions we rely on BaseException.__init__ for proper
# initialization.
if is_exc:
vals = ",".join("self." + a.name for a in attrs if a.init)
lines.append("BaseException.__init__(self, %s)" % (vals,))
args = ", ".join(args)
if kw_only_args:
if PY2:
+3 -3
View File
@@ -1,5 +1,5 @@
from typing import Union
from typing import Union, Any
from . import Attribute, _FilterType
def include(*what: Union[type, Attribute]) -> _FilterType: ...
def exclude(*what: Union[type, Attribute]) -> _FilterType: ...
def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...
def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ...
+113 -1
View File
@@ -136,7 +136,7 @@ class _InValidator(object):
def __call__(self, inst, attr, value):
try:
in_options = value in self.options
except TypeError as e: # e.g. `1 in "abc"`
except TypeError: # e.g. `1 in "abc"`
in_options = False
if not in_options:
@@ -168,3 +168,115 @@ def in_(options):
.. versionadded:: 17.1.0
"""
return _InValidator(options)
@attrs(repr=False, slots=False, hash=True)
class _IsCallableValidator(object):
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not callable(value):
raise TypeError("'{name}' must be callable".format(name=attr.name))
def __repr__(self):
return "<is_callable validator>"
def is_callable():
"""
A validator that raises a :class:`TypeError` if the initializer is called
with a value for this particular attribute that is not callable.
.. versionadded:: 19.1.0
:raises TypeError: With a human readable error message containing the
attribute (of type :class:`attr.Attribute`) name.
"""
return _IsCallableValidator()
@attrs(repr=False, slots=True, hash=True)
class _DeepIterable(object):
member_validator = attrib(validator=is_callable())
iterable_validator = attrib(
default=None, validator=optional(is_callable())
)
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if self.iterable_validator is not None:
self.iterable_validator(inst, attr, value)
for member in value:
self.member_validator(inst, attr, member)
def __repr__(self):
iterable_identifier = (
""
if self.iterable_validator is None
else " {iterable!r}".format(iterable=self.iterable_validator)
)
return (
"<deep_iterable validator for{iterable_identifier}"
" iterables of {member!r}>"
).format(
iterable_identifier=iterable_identifier,
member=self.member_validator,
)
def deep_iterable(member_validator, iterable_validator=None):
"""
A validator that performs deep validation of an iterable.
:param member_validator: Validator to apply to iterable members
:param iterable_validator: Validator to apply to iterable itself
(optional)
.. versionadded:: 19.1.0
:raises TypeError: if any sub-validators fail
"""
return _DeepIterable(member_validator, iterable_validator)
@attrs(repr=False, slots=True, hash=True)
class _DeepMapping(object):
key_validator = attrib(validator=is_callable())
value_validator = attrib(validator=is_callable())
mapping_validator = attrib(default=None, validator=optional(is_callable()))
def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if self.mapping_validator is not None:
self.mapping_validator(inst, attr, value)
for key in value:
self.key_validator(inst, attr, key)
self.value_validator(inst, attr, value[key])
def __repr__(self):
return (
"<deep_mapping validator for objects mapping {key!r} to {value!r}>"
).format(key=self.key_validator, value=self.value_validator)
def deep_mapping(key_validator, value_validator, mapping_validator=None):
"""
A validator that performs deep validation of a dictionary.
:param key_validator: Validator to apply to dictionary keys
:param value_validator: Validator to apply to dictionary values
:param mapping_validator: Validator to apply to top-level mapping
attribute (optional)
.. versionadded:: 19.1.0
:raises TypeError: if any sub-validators fail
"""
return _DeepMapping(key_validator, value_validator, mapping_validator)
+10
View File
@@ -12,3 +12,13 @@ def optional(
) -> _ValidatorType[Optional[_T]]: ...
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
def deep_iterable(
member_validator: _ValidatorType[_T],
iterable_validator: Optional[_ValidatorType[_T]],
) -> _ValidatorType[_T]: ...
def deep_mapping(
key_validator: _ValidatorType[_T],
value_validator: _ValidatorType[_T],
mapping_validator: Optional[_ValidatorType[_T]],
) -> _ValidatorType[_T]: ...
def is_callable() -> _ValidatorType[_T]: ...
-5
View File
@@ -1,5 +0,0 @@
This work is licensed under the Creative Commons
Attribution-ShareAlike 2.5 International License. To view a copy of
this license, visit http://creativecommons.org/licenses/by-sa/2.5/ or
send a letter to Creative Commons, PO Box 1866, Mountain View,
CA 94042, USA.
-4
View File
@@ -1,4 +0,0 @@
from .cursor import hide, show, HiddenCursor
__all__ = ["hide", "show", "HiddenCursor"]
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
## Author: James Spencer: http://stackoverflow.com/users/1375885/james-spencer
## Packager: Gijs TImmers: https://github.com/GijsTimmers
## Based on James Spencer's answer on StackOverflow:
## http://stackoverflow.com/questions/5174810/how-to-turn-off-blinking-cursor-in-command-window
## Licence: CC-BY-SA-2.5
## http://creativecommons.org/licenses/by-sa/2.5/
## This work is licensed under the Creative Commons
## Attribution-ShareAlike 2.5 International License. To view a copy of
## this license, visit http://creativecommons.org/licenses/by-sa/2.5/ or
## send a letter to Creative Commons, PO Box 1866, Mountain View,
## CA 94042, USA.
import sys
import os
if os.name == 'nt':
import ctypes
class _CursorInfo(ctypes.Structure):
_fields_ = [("size", ctypes.c_int),
("visible", ctypes.c_byte)]
def hide(stream=sys.stdout):
if os.name == 'nt':
ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))
ci.visible = False
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
elif os.name == 'posix':
stream.write("\033[?25l")
stream.flush()
def show(stream=sys.stdout):
if os.name == 'nt':
ci = _CursorInfo()
handle = ctypes.windll.kernel32.GetStdHandle(-11)
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))
ci.visible = True
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
elif os.name == 'posix':
stream.write("\033[?25h")
stream.flush()
class HiddenCursor(object):
def __init__(self, stream=sys.stdout):
self._stream = stream
def __enter__(self):
hide(stream=self._stream)
def __exit__(self, type, value, traceback):
show(stream=self._stream)
-33
View File
@@ -1,33 +0,0 @@
Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS
for more details.
Some rights reserved.
Redistribution and use in source and binary forms of the software as well
as documentation, with or without modification, are permitted provided
that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
+28
View File
@@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+140 -118
View File
@@ -1,75 +1,74 @@
# -*- coding: utf-8 -*-
"""
markupsafe
~~~~~~~~~~
markupsafe
~~~~~~~~~~
Implements a Markup string.
Implements an escape function and a Markup string to replace HTML
special characters with safe representations.
:copyright: (c) 2010 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
:copyright: 2010 Pallets
:license: BSD-3-Clause
"""
import re
import string
from collections import Mapping
from markupsafe._compat import text_type, string_types, int_types, \
unichr, iteritems, PY2
__version__ = "1.0"
from ._compat import int_types
from ._compat import iteritems
from ._compat import Mapping
from ._compat import PY2
from ._compat import string_types
from ._compat import text_type
from ._compat import unichr
__all__ = ['Markup', 'soft_unicode', 'escape', 'escape_silent']
__version__ = "1.1.1"
__all__ = ["Markup", "soft_unicode", "escape", "escape_silent"]
_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
_entity_re = re.compile(r'&([^& ;]+);')
_striptags_re = re.compile(r"(<!--.*?-->|<[^>]*>)")
_entity_re = re.compile(r"&([^& ;]+);")
class Markup(text_type):
r"""Marks a string as being safe for inclusion in HTML/XML output without
needing to be escaped. This implements the `__html__` interface a couple
of frameworks and web applications use. :class:`Markup` is a direct
subclass of `unicode` and provides all the methods of `unicode` just that
it escapes arguments passed and always returns `Markup`.
"""A string that is ready to be safely inserted into an HTML or XML
document, either because it was escaped or because it was marked
safe.
The `escape` function returns markup objects so that double escaping can't
happen.
Passing an object to the constructor converts it to text and wraps
it to mark it safe without escaping. To escape the text, use the
:meth:`escape` class method instead.
The constructor of the :class:`Markup` class can be used for three
different things: When passed an unicode object it's assumed to be safe,
when passed an object with an HTML representation (has an `__html__`
method) that representation is used, otherwise the object passed is
converted into a unicode string and then assumed to be safe:
>>> Markup('Hello, <em>World</em>!')
Markup('Hello, <em>World</em>!')
>>> Markup(42)
Markup('42')
>>> Markup.escape('Hello, <em>World</em>!')
Markup('Hello &lt;em&gt;World&lt;/em&gt;!')
>>> Markup("Hello <em>World</em>!")
Markup(u'Hello <em>World</em>!')
>>> class Foo(object):
... def __html__(self):
... return '<a href="#">foo</a>'
This implements the ``__html__()`` interface that some frameworks
use. Passing an object that implements ``__html__()`` will wrap the
output of that method, marking it safe.
>>> class Foo:
... def __html__(self):
... return '<a href="/foo">foo</a>'
...
>>> Markup(Foo())
Markup(u'<a href="#">foo</a>')
Markup('<a href="/foo">foo</a>')
If you want object passed being always treated as unsafe you can use the
:meth:`escape` classmethod to create a :class:`Markup` object:
This is a subclass of the text type (``str`` in Python 3,
``unicode`` in Python 2). It has the same methods as that type, but
all methods escape their arguments and return a ``Markup`` instance.
>>> Markup.escape("Hello <em>World</em>!")
Markup(u'Hello &lt;em&gt;World&lt;/em&gt;!')
Operations on a markup string are markup aware which means that all
arguments are passed through the :func:`escape` function:
>>> em = Markup("<em>%s</em>")
>>> em % "foo & bar"
Markup(u'<em>foo &amp; bar</em>')
>>> strong = Markup("<strong>%(text)s</strong>")
>>> strong % {'text': '<blink>hacker here</blink>'}
Markup(u'<strong>&lt;blink&gt;hacker here&lt;/blink&gt;</strong>')
>>> Markup("<em>Hello</em> ") + "<foo>"
Markup(u'<em>Hello</em> &lt;foo&gt;')
>>> Markup('<em>%s</em>') % 'foo & bar'
Markup('<em>foo &amp; bar</em>')
>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
"""
__slots__ = ()
def __new__(cls, base=u'', encoding=None, errors='strict'):
if hasattr(base, '__html__'):
def __new__(cls, base=u"", encoding=None, errors="strict"):
if hasattr(base, "__html__"):
base = base.__html__()
if encoding is None:
return text_type.__new__(cls, base)
@@ -79,12 +78,12 @@ class Markup(text_type):
return self
def __add__(self, other):
if isinstance(other, string_types) or hasattr(other, '__html__'):
if isinstance(other, string_types) or hasattr(other, "__html__"):
return self.__class__(super(Markup, self).__add__(self.escape(other)))
return NotImplemented
def __radd__(self, other):
if hasattr(other, '__html__') or isinstance(other, string_types):
if hasattr(other, "__html__") or isinstance(other, string_types):
return self.escape(other).__add__(self)
return NotImplemented
@@ -92,6 +91,7 @@ class Markup(text_type):
if isinstance(num, int_types):
return self.__class__(text_type.__mul__(self, num))
return NotImplemented
__rmul__ = __mul__
def __mod__(self, arg):
@@ -102,115 +102,124 @@ class Markup(text_type):
return self.__class__(text_type.__mod__(self, arg))
def __repr__(self):
return '%s(%s)' % (
self.__class__.__name__,
text_type.__repr__(self)
)
return "%s(%s)" % (self.__class__.__name__, text_type.__repr__(self))
def join(self, seq):
return self.__class__(text_type.join(self, map(self.escape, seq)))
join.__doc__ = text_type.join.__doc__
def split(self, *args, **kwargs):
return list(map(self.__class__, text_type.split(self, *args, **kwargs)))
split.__doc__ = text_type.split.__doc__
def rsplit(self, *args, **kwargs):
return list(map(self.__class__, text_type.rsplit(self, *args, **kwargs)))
rsplit.__doc__ = text_type.rsplit.__doc__
def splitlines(self, *args, **kwargs):
return list(map(self.__class__, text_type.splitlines(
self, *args, **kwargs)))
return list(map(self.__class__, text_type.splitlines(self, *args, **kwargs)))
splitlines.__doc__ = text_type.splitlines.__doc__
def unescape(self):
r"""Unescape markup again into an text_type string. This also resolves
known HTML4 and XHTML entities:
"""Convert escaped markup back into a text string. This replaces
HTML entities with the characters they represent.
>>> Markup("Main &raquo; <em>About</em>").unescape()
u'Main \xbb <em>About</em>'
>>> Markup('Main &raquo; <em>About</em>').unescape()
'Main » <em>About</em>'
"""
from markupsafe._constants import HTML_ENTITIES
from ._constants import HTML_ENTITIES
def handle_match(m):
name = m.group(1)
if name in HTML_ENTITIES:
return unichr(HTML_ENTITIES[name])
try:
if name[:2] in ('#x', '#X'):
if name[:2] in ("#x", "#X"):
return unichr(int(name[2:], 16))
elif name.startswith('#'):
elif name.startswith("#"):
return unichr(int(name[1:]))
except ValueError:
pass
# Don't modify unexpected input.
return m.group()
return _entity_re.sub(handle_match, text_type(self))
def striptags(self):
r"""Unescape markup into an text_type string and strip all tags. This
also resolves known HTML4 and XHTML entities. Whitespace is
normalized to one:
""":meth:`unescape` the markup, remove tags, and normalize
whitespace to single spaces.
>>> Markup("Main &raquo; <em>About</em>").striptags()
u'Main \xbb About'
>>> Markup('Main &raquo;\t<em>About</em>').striptags()
'Main » About'
"""
stripped = u' '.join(_striptags_re.sub('', self).split())
stripped = u" ".join(_striptags_re.sub("", self).split())
return Markup(stripped).unescape()
@classmethod
def escape(cls, s):
"""Escape the string. Works like :func:`escape` with the difference
that for subclasses of :class:`Markup` this function would return the
correct subclass.
"""Escape a string. Calls :func:`escape` and ensures that for
subclasses the correct type is returned.
"""
rv = escape(s)
if rv.__class__ is not cls:
return cls(rv)
return rv
def make_simple_escaping_wrapper(name):
def make_simple_escaping_wrapper(name): # noqa: B902
orig = getattr(text_type, name)
def func(self, *args, **kwargs):
args = _escape_argspec(list(args), enumerate(args), self.escape)
_escape_argspec(kwargs, iteritems(kwargs), self.escape)
return self.__class__(orig(self, *args, **kwargs))
func.__name__ = orig.__name__
func.__doc__ = orig.__doc__
return func
for method in '__getitem__', 'capitalize', \
'title', 'lower', 'upper', 'replace', 'ljust', \
'rjust', 'lstrip', 'rstrip', 'center', 'strip', \
'translate', 'expandtabs', 'swapcase', 'zfill':
for method in (
"__getitem__",
"capitalize",
"title",
"lower",
"upper",
"replace",
"ljust",
"rjust",
"lstrip",
"rstrip",
"center",
"strip",
"translate",
"expandtabs",
"swapcase",
"zfill",
):
locals()[method] = make_simple_escaping_wrapper(method)
# new in python 2.5
if hasattr(text_type, 'partition'):
def partition(self, sep):
return tuple(map(self.__class__,
text_type.partition(self, self.escape(sep))))
def rpartition(self, sep):
return tuple(map(self.__class__,
text_type.rpartition(self, self.escape(sep))))
def partition(self, sep):
return tuple(map(self.__class__, text_type.partition(self, self.escape(sep))))
# new in python 2.6
if hasattr(text_type, 'format'):
def format(*args, **kwargs):
self, args = args[0], args[1:]
formatter = EscapeFormatter(self.escape)
kwargs = _MagicFormatMapping(args, kwargs)
return self.__class__(formatter.vformat(self, args, kwargs))
def rpartition(self, sep):
return tuple(map(self.__class__, text_type.rpartition(self, self.escape(sep))))
def __html_format__(self, format_spec):
if format_spec:
raise ValueError('Unsupported format specification '
'for Markup.')
return self
def format(self, *args, **kwargs):
formatter = EscapeFormatter(self.escape)
kwargs = _MagicFormatMapping(args, kwargs)
return self.__class__(formatter.vformat(self, args, kwargs))
def __html_format__(self, format_spec):
if format_spec:
raise ValueError("Unsupported format specification " "for Markup.")
return self
# not in python 3
if hasattr(text_type, '__getslice__'):
__getslice__ = make_simple_escaping_wrapper('__getslice__')
if hasattr(text_type, "__getslice__"):
__getslice__ = make_simple_escaping_wrapper("__getslice__")
del method, make_simple_escaping_wrapper
@@ -229,7 +238,7 @@ class _MagicFormatMapping(Mapping):
self._last_index = 0
def __getitem__(self, key):
if key == '':
if key == "":
idx = self._last_index
self._last_index += 1
try:
@@ -246,35 +255,37 @@ class _MagicFormatMapping(Mapping):
return len(self._kwargs)
if hasattr(text_type, 'format'):
class EscapeFormatter(string.Formatter):
if hasattr(text_type, "format"):
class EscapeFormatter(string.Formatter):
def __init__(self, escape):
self.escape = escape
def format_field(self, value, format_spec):
if hasattr(value, '__html_format__'):
if hasattr(value, "__html_format__"):
rv = value.__html_format__(format_spec)
elif hasattr(value, '__html__'):
elif hasattr(value, "__html__"):
if format_spec:
raise ValueError('No format specification allowed '
'when formatting an object with '
'its __html__ method.')
raise ValueError(
"Format specifier {0} given, but {1} does not"
" define __html_format__. A class that defines"
" __html__ must define __html_format__ to work"
" with format specifiers.".format(format_spec, type(value))
)
rv = value.__html__()
else:
# We need to make sure the format spec is unicode here as
# otherwise the wrong callback methods are invoked. For
# instance a byte string there would invoke __str__ and
# not __unicode__.
rv = string.Formatter.format_field(
self, value, text_type(format_spec))
rv = string.Formatter.format_field(self, value, text_type(format_spec))
return text_type(self.escape(rv))
def _escape_argspec(obj, iterable, escape):
"""Helper for various string-wrapped functions."""
for key, value in iterable:
if hasattr(value, '__html__') or isinstance(value, string_types):
if hasattr(value, "__html__") or isinstance(value, string_types):
obj[key] = escape(value)
return obj
@@ -286,20 +297,31 @@ class _MarkupEscapeHelper(object):
self.obj = obj
self.escape = escape
__getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x], s.escape)
__unicode__ = __str__ = lambda s: text_type(s.escape(s.obj))
__repr__ = lambda s: str(s.escape(repr(s.obj)))
__int__ = lambda s: int(s.obj)
__float__ = lambda s: float(s.obj)
def __getitem__(self, item):
return _MarkupEscapeHelper(self.obj[item], self.escape)
def __str__(self):
return text_type(self.escape(self.obj))
__unicode__ = __str__
def __repr__(self):
return str(self.escape(repr(self.obj)))
def __int__(self):
return int(self.obj)
def __float__(self):
return float(self.obj)
# we have to import it down here as the speedups and native
# modules imports the markup type which is define above.
try:
from markupsafe._speedups import escape, escape_silent, soft_unicode
from ._speedups import escape, escape_silent, soft_unicode
except ImportError:
from markupsafe._native import escape, escape_silent, soft_unicode
from ._native import escape, escape_silent, soft_unicode
if not PY2:
soft_str = soft_unicode
__all__.append('soft_str')
__all__.append("soft_str")
+15 -8
View File
@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
"""
markupsafe._compat
~~~~~~~~~~~~~~~~~~
markupsafe._compat
~~~~~~~~~~~~~~~~~~
Compatibility module for different Python versions.
:copyright: (c) 2013 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
:copyright: 2010 Pallets
:license: BSD-3-Clause
"""
import sys
@@ -17,10 +15,19 @@ if not PY2:
string_types = (str,)
unichr = chr
int_types = (int,)
iteritems = lambda x: iter(x.items())
def iteritems(x):
return iter(x.items())
from collections.abc import Mapping
else:
text_type = unicode
string_types = (str, unicode)
unichr = unichr
int_types = (int, long)
iteritems = lambda x: x.iteritems()
def iteritems(x):
return x.iteritems()
from collections import Mapping
+257 -260
View File
@@ -1,267 +1,264 @@
# -*- coding: utf-8 -*-
"""
markupsafe._constants
~~~~~~~~~~~~~~~~~~~~~
markupsafe._constants
~~~~~~~~~~~~~~~~~~~~~
Highlevel implementation of the Markup string.
:copyright: (c) 2010 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
:copyright: 2010 Pallets
:license: BSD-3-Clause
"""
HTML_ENTITIES = {
'AElig': 198,
'Aacute': 193,
'Acirc': 194,
'Agrave': 192,
'Alpha': 913,
'Aring': 197,
'Atilde': 195,
'Auml': 196,
'Beta': 914,
'Ccedil': 199,
'Chi': 935,
'Dagger': 8225,
'Delta': 916,
'ETH': 208,
'Eacute': 201,
'Ecirc': 202,
'Egrave': 200,
'Epsilon': 917,
'Eta': 919,
'Euml': 203,
'Gamma': 915,
'Iacute': 205,
'Icirc': 206,
'Igrave': 204,
'Iota': 921,
'Iuml': 207,
'Kappa': 922,
'Lambda': 923,
'Mu': 924,
'Ntilde': 209,
'Nu': 925,
'OElig': 338,
'Oacute': 211,
'Ocirc': 212,
'Ograve': 210,
'Omega': 937,
'Omicron': 927,
'Oslash': 216,
'Otilde': 213,
'Ouml': 214,
'Phi': 934,
'Pi': 928,
'Prime': 8243,
'Psi': 936,
'Rho': 929,
'Scaron': 352,
'Sigma': 931,
'THORN': 222,
'Tau': 932,
'Theta': 920,
'Uacute': 218,
'Ucirc': 219,
'Ugrave': 217,
'Upsilon': 933,
'Uuml': 220,
'Xi': 926,
'Yacute': 221,
'Yuml': 376,
'Zeta': 918,
'aacute': 225,
'acirc': 226,
'acute': 180,
'aelig': 230,
'agrave': 224,
'alefsym': 8501,
'alpha': 945,
'amp': 38,
'and': 8743,
'ang': 8736,
'apos': 39,
'aring': 229,
'asymp': 8776,
'atilde': 227,
'auml': 228,
'bdquo': 8222,
'beta': 946,
'brvbar': 166,
'bull': 8226,
'cap': 8745,
'ccedil': 231,
'cedil': 184,
'cent': 162,
'chi': 967,
'circ': 710,
'clubs': 9827,
'cong': 8773,
'copy': 169,
'crarr': 8629,
'cup': 8746,
'curren': 164,
'dArr': 8659,
'dagger': 8224,
'darr': 8595,
'deg': 176,
'delta': 948,
'diams': 9830,
'divide': 247,
'eacute': 233,
'ecirc': 234,
'egrave': 232,
'empty': 8709,
'emsp': 8195,
'ensp': 8194,
'epsilon': 949,
'equiv': 8801,
'eta': 951,
'eth': 240,
'euml': 235,
'euro': 8364,
'exist': 8707,
'fnof': 402,
'forall': 8704,
'frac12': 189,
'frac14': 188,
'frac34': 190,
'frasl': 8260,
'gamma': 947,
'ge': 8805,
'gt': 62,
'hArr': 8660,
'harr': 8596,
'hearts': 9829,
'hellip': 8230,
'iacute': 237,
'icirc': 238,
'iexcl': 161,
'igrave': 236,
'image': 8465,
'infin': 8734,
'int': 8747,
'iota': 953,
'iquest': 191,
'isin': 8712,
'iuml': 239,
'kappa': 954,
'lArr': 8656,
'lambda': 955,
'lang': 9001,
'laquo': 171,
'larr': 8592,
'lceil': 8968,
'ldquo': 8220,
'le': 8804,
'lfloor': 8970,
'lowast': 8727,
'loz': 9674,
'lrm': 8206,
'lsaquo': 8249,
'lsquo': 8216,
'lt': 60,
'macr': 175,
'mdash': 8212,
'micro': 181,
'middot': 183,
'minus': 8722,
'mu': 956,
'nabla': 8711,
'nbsp': 160,
'ndash': 8211,
'ne': 8800,
'ni': 8715,
'not': 172,
'notin': 8713,
'nsub': 8836,
'ntilde': 241,
'nu': 957,
'oacute': 243,
'ocirc': 244,
'oelig': 339,
'ograve': 242,
'oline': 8254,
'omega': 969,
'omicron': 959,
'oplus': 8853,
'or': 8744,
'ordf': 170,
'ordm': 186,
'oslash': 248,
'otilde': 245,
'otimes': 8855,
'ouml': 246,
'para': 182,
'part': 8706,
'permil': 8240,
'perp': 8869,
'phi': 966,
'pi': 960,
'piv': 982,
'plusmn': 177,
'pound': 163,
'prime': 8242,
'prod': 8719,
'prop': 8733,
'psi': 968,
'quot': 34,
'rArr': 8658,
'radic': 8730,
'rang': 9002,
'raquo': 187,
'rarr': 8594,
'rceil': 8969,
'rdquo': 8221,
'real': 8476,
'reg': 174,
'rfloor': 8971,
'rho': 961,
'rlm': 8207,
'rsaquo': 8250,
'rsquo': 8217,
'sbquo': 8218,
'scaron': 353,
'sdot': 8901,
'sect': 167,
'shy': 173,
'sigma': 963,
'sigmaf': 962,
'sim': 8764,
'spades': 9824,
'sub': 8834,
'sube': 8838,
'sum': 8721,
'sup': 8835,
'sup1': 185,
'sup2': 178,
'sup3': 179,
'supe': 8839,
'szlig': 223,
'tau': 964,
'there4': 8756,
'theta': 952,
'thetasym': 977,
'thinsp': 8201,
'thorn': 254,
'tilde': 732,
'times': 215,
'trade': 8482,
'uArr': 8657,
'uacute': 250,
'uarr': 8593,
'ucirc': 251,
'ugrave': 249,
'uml': 168,
'upsih': 978,
'upsilon': 965,
'uuml': 252,
'weierp': 8472,
'xi': 958,
'yacute': 253,
'yen': 165,
'yuml': 255,
'zeta': 950,
'zwj': 8205,
'zwnj': 8204
"AElig": 198,
"Aacute": 193,
"Acirc": 194,
"Agrave": 192,
"Alpha": 913,
"Aring": 197,
"Atilde": 195,
"Auml": 196,
"Beta": 914,
"Ccedil": 199,
"Chi": 935,
"Dagger": 8225,
"Delta": 916,
"ETH": 208,
"Eacute": 201,
"Ecirc": 202,
"Egrave": 200,
"Epsilon": 917,
"Eta": 919,
"Euml": 203,
"Gamma": 915,
"Iacute": 205,
"Icirc": 206,
"Igrave": 204,
"Iota": 921,
"Iuml": 207,
"Kappa": 922,
"Lambda": 923,
"Mu": 924,
"Ntilde": 209,
"Nu": 925,
"OElig": 338,
"Oacute": 211,
"Ocirc": 212,
"Ograve": 210,
"Omega": 937,
"Omicron": 927,
"Oslash": 216,
"Otilde": 213,
"Ouml": 214,
"Phi": 934,
"Pi": 928,
"Prime": 8243,
"Psi": 936,
"Rho": 929,
"Scaron": 352,
"Sigma": 931,
"THORN": 222,
"Tau": 932,
"Theta": 920,
"Uacute": 218,
"Ucirc": 219,
"Ugrave": 217,
"Upsilon": 933,
"Uuml": 220,
"Xi": 926,
"Yacute": 221,
"Yuml": 376,
"Zeta": 918,
"aacute": 225,
"acirc": 226,
"acute": 180,
"aelig": 230,
"agrave": 224,
"alefsym": 8501,
"alpha": 945,
"amp": 38,
"and": 8743,
"ang": 8736,
"apos": 39,
"aring": 229,
"asymp": 8776,
"atilde": 227,
"auml": 228,
"bdquo": 8222,
"beta": 946,
"brvbar": 166,
"bull": 8226,
"cap": 8745,
"ccedil": 231,
"cedil": 184,
"cent": 162,
"chi": 967,
"circ": 710,
"clubs": 9827,
"cong": 8773,
"copy": 169,
"crarr": 8629,
"cup": 8746,
"curren": 164,
"dArr": 8659,
"dagger": 8224,
"darr": 8595,
"deg": 176,
"delta": 948,
"diams": 9830,
"divide": 247,
"eacute": 233,
"ecirc": 234,
"egrave": 232,
"empty": 8709,
"emsp": 8195,
"ensp": 8194,
"epsilon": 949,
"equiv": 8801,
"eta": 951,
"eth": 240,
"euml": 235,
"euro": 8364,
"exist": 8707,
"fnof": 402,
"forall": 8704,
"frac12": 189,
"frac14": 188,
"frac34": 190,
"frasl": 8260,
"gamma": 947,
"ge": 8805,
"gt": 62,
"hArr": 8660,
"harr": 8596,
"hearts": 9829,
"hellip": 8230,
"iacute": 237,
"icirc": 238,
"iexcl": 161,
"igrave": 236,
"image": 8465,
"infin": 8734,
"int": 8747,
"iota": 953,
"iquest": 191,
"isin": 8712,
"iuml": 239,
"kappa": 954,
"lArr": 8656,
"lambda": 955,
"lang": 9001,
"laquo": 171,
"larr": 8592,
"lceil": 8968,
"ldquo": 8220,
"le": 8804,
"lfloor": 8970,
"lowast": 8727,
"loz": 9674,
"lrm": 8206,
"lsaquo": 8249,
"lsquo": 8216,
"lt": 60,
"macr": 175,
"mdash": 8212,
"micro": 181,
"middot": 183,
"minus": 8722,
"mu": 956,
"nabla": 8711,
"nbsp": 160,
"ndash": 8211,
"ne": 8800,
"ni": 8715,
"not": 172,
"notin": 8713,
"nsub": 8836,
"ntilde": 241,
"nu": 957,
"oacute": 243,
"ocirc": 244,
"oelig": 339,
"ograve": 242,
"oline": 8254,
"omega": 969,
"omicron": 959,
"oplus": 8853,
"or": 8744,
"ordf": 170,
"ordm": 186,
"oslash": 248,
"otilde": 245,
"otimes": 8855,
"ouml": 246,
"para": 182,
"part": 8706,
"permil": 8240,
"perp": 8869,
"phi": 966,
"pi": 960,
"piv": 982,
"plusmn": 177,
"pound": 163,
"prime": 8242,
"prod": 8719,
"prop": 8733,
"psi": 968,
"quot": 34,
"rArr": 8658,
"radic": 8730,
"rang": 9002,
"raquo": 187,
"rarr": 8594,
"rceil": 8969,
"rdquo": 8221,
"real": 8476,
"reg": 174,
"rfloor": 8971,
"rho": 961,
"rlm": 8207,
"rsaquo": 8250,
"rsquo": 8217,
"sbquo": 8218,
"scaron": 353,
"sdot": 8901,
"sect": 167,
"shy": 173,
"sigma": 963,
"sigmaf": 962,
"sim": 8764,
"spades": 9824,
"sub": 8834,
"sube": 8838,
"sum": 8721,
"sup": 8835,
"sup1": 185,
"sup2": 178,
"sup3": 179,
"supe": 8839,
"szlig": 223,
"tau": 964,
"there4": 8756,
"theta": 952,
"thetasym": 977,
"thinsp": 8201,
"thorn": 254,
"tilde": 732,
"times": 215,
"trade": 8482,
"uArr": 8657,
"uacute": 250,
"uarr": 8593,
"ucirc": 251,
"ugrave": 249,
"uml": 168,
"upsih": 978,
"upsilon": 965,
"uuml": 252,
"weierp": 8472,
"xi": 958,
"yacute": 253,
"yen": 165,
"yuml": 255,
"zeta": 950,
"zwj": 8205,
"zwnj": 8204,
}
+45 -22
View File
@@ -1,36 +1,49 @@
# -*- coding: utf-8 -*-
"""
markupsafe._native
~~~~~~~~~~~~~~~~~~
markupsafe._native
~~~~~~~~~~~~~~~~~~
Native Python implementation the C module is not compiled.
Native Python implementation used when the C module is not compiled.
:copyright: (c) 2010 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
:copyright: 2010 Pallets
:license: BSD-3-Clause
"""
from markupsafe import Markup
from markupsafe._compat import text_type
from . import Markup
from ._compat import text_type
def escape(s):
"""Convert the characters &, <, >, ' and " in string s to HTML-safe
sequences. Use this if you need to display text that might contain
such characters in HTML. Marks return value as markup string.
"""Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in
the string with HTML-safe sequences. Use this if you need to display
text that might contain such characters in HTML.
If the object has an ``__html__`` method, it is called and the
return value is assumed to already be safe for HTML.
:param s: An object to be converted to a string and escaped.
:return: A :class:`Markup` string with the escaped text.
"""
if hasattr(s, '__html__'):
return s.__html__()
return Markup(text_type(s)
.replace('&', '&amp;')
.replace('>', '&gt;')
.replace('<', '&lt;')
.replace("'", '&#39;')
.replace('"', '&#34;')
if hasattr(s, "__html__"):
return Markup(s.__html__())
return Markup(
text_type(s)
.replace("&", "&amp;")
.replace(">", "&gt;")
.replace("<", "&lt;")
.replace("'", "&#39;")
.replace('"', "&#34;")
)
def escape_silent(s):
"""Like :func:`escape` but converts `None` into an empty
markup string.
"""Like :func:`escape` but treats ``None`` as the empty string.
Useful with optional values, as otherwise you get the string
``'None'`` when the value is ``None``.
>>> escape(None)
Markup('None')
>>> escape_silent(None)
Markup('')
"""
if s is None:
return Markup()
@@ -38,8 +51,18 @@ def escape_silent(s):
def soft_unicode(s):
"""Make a string unicode if it isn't already. That way a markup
string is not converted back to unicode.
"""Convert an object to a string if it isn't already. This preserves
a :class:`Markup` string rather than converting it back to a basic
string, so it will still be marked as safe and won't be escaped
again.
>>> value = escape('<User 1>')
>>> value
Markup('&lt;User 1&gt;')
>>> escape(str(value))
Markup('&amp;lt;User 1&amp;gt;')
>>> escape(soft_unicode(value))
Markup('&lt;User 1&gt;')
"""
if not isinstance(s, text_type):
s = text_type(s)
+199 -15
View File
@@ -2,33 +2,30 @@
* markupsafe._speedups
* ~~~~~~~~~~~~~~~~~~~~
*
* This module implements functions for automatic escaping in C for better
* performance.
* C implementation of escaping for better performance. Used instead of
* the native Python implementation when compiled.
*
* :copyright: (c) 2010 by Armin Ronacher.
* :license: BSD.
* :copyright: 2010 Pallets
* :license: BSD-3-Clause
*/
#include <Python.h>
#if PY_MAJOR_VERSION < 3
#define ESCAPED_CHARS_TABLE_SIZE 63
#define UNICHR(x) (PyUnicode_AS_UNICODE((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL)));
#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
typedef int Py_ssize_t;
#define PY_SSIZE_T_MAX INT_MAX
#define PY_SSIZE_T_MIN INT_MIN
#endif
static PyObject* markup;
static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE];
static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE];
#endif
static PyObject* markup;
static int
init_constants(void)
{
PyObject *module;
#if PY_MAJOR_VERSION < 3
/* mapping of characters to replace */
escaped_chars_repl['"'] = UNICHR("&#34;");
escaped_chars_repl['\''] = UNICHR("&#39;");
@@ -41,6 +38,7 @@ init_constants(void)
escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \
escaped_chars_delta_len['&'] = 4;
escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3;
#endif
/* import markup type so that we can mark the return value */
module = PyImport_ImportModule("markupsafe");
@@ -52,6 +50,7 @@ init_constants(void)
return 1;
}
#if PY_MAJOR_VERSION < 3
static PyObject*
escape_unicode(PyUnicodeObject *in)
{
@@ -112,13 +111,192 @@ escape_unicode(PyUnicodeObject *in)
return (PyObject*)out;
}
#else /* PY_MAJOR_VERSION < 3 */
#define GET_DELTA(inp, inp_end, delta) \
while (inp < inp_end) { \
switch (*inp++) { \
case '"': \
case '\'': \
case '&': \
delta += 4; \
break; \
case '<': \
case '>': \
delta += 3; \
break; \
} \
}
#define DO_ESCAPE(inp, inp_end, outp) \
{ \
Py_ssize_t ncopy = 0; \
while (inp < inp_end) { \
switch (*inp) { \
case '"': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = '#'; \
*outp++ = '3'; \
*outp++ = '4'; \
*outp++ = ';'; \
break; \
case '\'': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = '#'; \
*outp++ = '3'; \
*outp++ = '9'; \
*outp++ = ';'; \
break; \
case '&': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = 'a'; \
*outp++ = 'm'; \
*outp++ = 'p'; \
*outp++ = ';'; \
break; \
case '<': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = 'l'; \
*outp++ = 't'; \
*outp++ = ';'; \
break; \
case '>': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = 'g'; \
*outp++ = 't'; \
*outp++ = ';'; \
break; \
default: \
ncopy++; \
} \
inp++; \
} \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
}
static PyObject*
escape_unicode_kind1(PyUnicodeObject *in)
{
Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in);
Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in);
Py_UCS1 *outp;
PyObject *out;
Py_ssize_t delta = 0;
GET_DELTA(inp, inp_end, delta);
if (!delta) {
Py_INCREF(in);
return (PyObject*)in;
}
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta,
PyUnicode_IS_ASCII(in) ? 127 : 255);
if (!out)
return NULL;
inp = PyUnicode_1BYTE_DATA(in);
outp = PyUnicode_1BYTE_DATA(out);
DO_ESCAPE(inp, inp_end, outp);
return out;
}
static PyObject*
escape_unicode_kind2(PyUnicodeObject *in)
{
Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in);
Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in);
Py_UCS2 *outp;
PyObject *out;
Py_ssize_t delta = 0;
GET_DELTA(inp, inp_end, delta);
if (!delta) {
Py_INCREF(in);
return (PyObject*)in;
}
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535);
if (!out)
return NULL;
inp = PyUnicode_2BYTE_DATA(in);
outp = PyUnicode_2BYTE_DATA(out);
DO_ESCAPE(inp, inp_end, outp);
return out;
}
static PyObject*
escape_unicode_kind4(PyUnicodeObject *in)
{
Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in);
Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in);
Py_UCS4 *outp;
PyObject *out;
Py_ssize_t delta = 0;
GET_DELTA(inp, inp_end, delta);
if (!delta) {
Py_INCREF(in);
return (PyObject*)in;
}
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111);
if (!out)
return NULL;
inp = PyUnicode_4BYTE_DATA(in);
outp = PyUnicode_4BYTE_DATA(out);
DO_ESCAPE(inp, inp_end, outp);
return out;
}
static PyObject*
escape_unicode(PyUnicodeObject *in)
{
if (PyUnicode_READY(in))
return NULL;
switch (PyUnicode_KIND(in)) {
case PyUnicode_1BYTE_KIND:
return escape_unicode_kind1(in);
case PyUnicode_2BYTE_KIND:
return escape_unicode_kind2(in);
case PyUnicode_4BYTE_KIND:
return escape_unicode_kind4(in);
}
assert(0); /* shouldn't happen */
return NULL;
}
#endif /* PY_MAJOR_VERSION < 3 */
static PyObject*
escape(PyObject *self, PyObject *text)
{
static PyObject *id_html;
PyObject *s = NULL, *rv = NULL, *html;
if (id_html == NULL) {
#if PY_MAJOR_VERSION < 3
id_html = PyString_InternFromString("__html__");
#else
id_html = PyUnicode_InternFromString("__html__");
#endif
if (id_html == NULL) {
return NULL;
}
}
/* we don't have to escape integers, bools or floats */
if (PyLong_CheckExact(text) ||
#if PY_MAJOR_VERSION < 3
@@ -129,10 +307,16 @@ escape(PyObject *self, PyObject *text)
return PyObject_CallFunctionObjArgs(markup, text, NULL);
/* if the object has an __html__ method that performs the escaping */
html = PyObject_GetAttrString(text, "__html__");
html = PyObject_GetAttr(text ,id_html);
if (html) {
rv = PyObject_CallObject(html, NULL);
s = PyObject_CallObject(html, NULL);
Py_DECREF(html);
if (s == NULL) {
return NULL;
}
/* Convert to Markup object */
rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
Py_DECREF(s);
return rv;
}
+31
View File
@@ -0,0 +1,31 @@
Build Amazing Things.
***
### Unlicense
This is free and unencumbered software released into the public\
domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute\
this software, either in source code form or as a compiled binary, for any\
purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of\
this software dedicate any and all copyright interest in the software to the\
public domain. We make this dedication for the benefit of the public at\
large and to the detriment of our heirs and successors. We intend this\
dedication to be an overt act of relinquishment in perpetuity of all\
present and future rights to this software under copyright law.
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 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.
For more information, please refer to <http://unlicense.org/>
+21
View File
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
#
# omdict - Ordered Multivalue Dictionary.
#
# Ansgar Grunseid
# grunseid.com
# grunseid@gmail.com
#
# License: Build Amazing Things (Unlicense)
from __future__ import absolute_import
from .orderedmultidict import * # noqa
__title__ = 'orderedmultidict'
__version__ = '1.0'
__author__ = 'Ansgar Grunseid'
__contact__ = 'grunseid@gmail.com'
__license__ = 'Unlicense'
__url__ = 'https://github.com/gruns/orderedmultidict'
+157
View File
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
#
# omdict - Ordered Multivalue Dictionary.
#
# Ansgar Grunseid
# grunseid.com
# grunseid@gmail.com
#
# License: Build Amazing Things (Unlicense)
from __future__ import absolute_import
from six.moves import zip_longest
_absent = object() # Marker that means no parameter was provided.
class itemnode(object):
"""
Dictionary key:value items wrapped in a node to be members of itemlist, the
doubly linked list defined below.
"""
def __init__(self, prev=None, next=None, key=_absent, value=_absent):
self.prev = prev
self.next = next
self.key = key
self.value = value
class itemlist(object):
"""
Doubly linked list of itemnodes.
This class is used as the key:value item storage of orderedmultidict.
Methods below were only added as needed for use with orderedmultidict, so
some otherwise common list methods may be missing.
"""
def __init__(self, items=[]):
self.root = itemnode()
self.root.next = self.root.prev = self.root
self.size = 0
for key, value in items:
self.append(key, value)
def append(self, key, value):
tail = self.root.prev if self.root.prev is not self.root else self.root
node = itemnode(tail, self.root, key=key, value=value)
tail.next = node
self.root.prev = node
self.size += 1
return node
def removenode(self, node):
node.prev.next = node.next
node.next.prev = node.prev
self.size -= 1
return self
def clear(self):
for node, key, value in self:
self.removenode(node)
return self
def items(self):
return list(self.iteritems())
def keys(self):
return list(self.iterkeys())
def values(self):
return list(self.itervalues())
def iteritems(self):
for node, key, value in self:
yield key, value
def iterkeys(self):
for node, key, value in self:
yield key
def itervalues(self):
for node, key, value in self:
yield value
def reverse(self):
for node, key, value in self:
node.prev, node.next = node.next, node.prev
self.root.prev, self.root.next = self.root.next, self.root.prev
return self
def __len__(self):
return self.size
def __iter__(self):
current = self.root.next
while current and current is not self.root:
# Record current.next here in case current.next changes after the
# yield and before we return for the next iteration. For example,
# methods like reverse() will change current.next() before yield
# gets executed again.
nextnode = current.next
yield current, current.key, current.value
current = nextnode
def __contains__(self, item):
"""
Params:
item: Can either be a (key,value) tuple or an itemnode reference.
"""
node = key = value = _absent
if hasattr(item, '__len__') and callable(item.__len__):
if len(item) == 2:
key, value = item
elif len(item) == 3:
node, key, value = item
else:
node = item
if node is not _absent or _absent not in [key, value]:
for selfnode, selfkey, selfvalue in self:
if ((node is _absent and key == selfkey and value == selfvalue)
or (node is not _absent and node == selfnode)):
return True
return False
def __getitem__(self, index):
# Only support direct access to the first or last element, as this is
# all orderedmultidict needs for now.
if index == 0 and self.root.next is not self.root:
return self.root.next
elif index == -1 and self.root.prev is not self.root:
return self.root.prev
raise IndexError(index)
def __delitem__(self, index):
self.removenode(self[index])
def __eq__(self, other):
for (n1, key1, value1), (n2, key2, value2) in zip_longest(self, other):
if key1 != key2 or value1 != value2:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def __nonzero__(self):
return self.size > 0
def __str__(self):
return '[%s]' % self.items()
+811
View File
@@ -0,0 +1,811 @@
# -*- coding: utf-8 -*-
#
# omdict - Ordered Multivalue Dictionary.
#
# Ansgar Grunseid
# grunseid.com
# grunseid@gmail.com
#
# License: Build Amazing Things (Unlicense)
from __future__ import absolute_import
from itertools import chain
from collections import MutableMapping
import six
from six.moves import map, zip_longest
from .itemlist import itemlist
try:
from collections import OrderedDict as odict # Python 2.7 and later.
except ImportError:
from ordereddict import OrderedDict as odict # Python 2.6 and earlier.
import sys
items_attr = 'items' if sys.version_info[0] >= 3 else 'iteritems'
_absent = object() # Marker that means no parameter was provided.
def callable_attr(obj, attr):
return hasattr(obj, attr) and callable(getattr(obj, attr))
#
# TODO(grun): Create a subclass of list that values(), getlist(), allitems(),
# etc return that the user can manipulate directly to control the omdict()
# object.
#
# For example, users should be able to do things like
#
# omd = omdict([(1,1), (1,11)])
# omd.values(1).append('sup')
# omd.allitems() == [(1,1), (1,11), (1,'sup')]
# omd.values(1).remove(11)
# omd.allitems() == [(1,1), (1,'sup')]
# omd.values(1).extend(['two', 'more'])
# omd.allitems() == [(1,1), (1,'sup'), (1,'two'), (1,'more')]
#
# or
#
# omd = omdict([(1,1), (1,11)])
# omd.allitems().extend([(2,2), (2,22)])
# omd.allitems() == [(1,1), (1,11), (2,2), (2,22)])
#
# or
#
# omd = omdict()
# omd.values(1) = [1, 11]
# omd.allitems() == [(1,1), (1,11)]
# omd.values(1) = list(map(lambda i: i * -10, omd.values(1)))
# omd.allitems() == [(1,-10), (1,-110)]
# omd.allitems() = filter(lambda (k,v): v > -100, omd.allitems())
# omd.allitems() == [(1,-10)]
#
# etc.
#
# To accomplish this, subclass list in such a manner that each list element is
# really a two tuple, where the first tuple value is the actual value and the
# second tuple value is a reference to the itemlist node for that value. Users
# only interact with the first tuple values, the actual values, but behind the
# scenes when an element is modified, deleted, inserted, etc, the according
# itemlist nodes are modified, deleted, inserted, etc accordingly. In this
# manner, users can manipulate omdict objects directly through direct list
# manipulation.
#
# Once accomplished, some methods become redundant and should be removed in
# favor of the more intuitive direct value list manipulation. Such redundant
# methods include getlist() (removed in favor of values()?), addlist(), and
# setlist().
#
# With the removal of many of the 'list' methods, think about renaming all
# remaining 'list' methods to 'values' methods, like poplist() -> popvalues(),
# poplistitem() -> popvaluesitem(), etc. This would be an easy switch for most
# methods, but wouldn't fit others so well. For example, iterlists() would
# become itervalues(), a name extremely similar to iterallvalues() but quite
# different in function.
#
class omdict(MutableMapping):
"""
Ordered Multivalue Dictionary.
A multivalue dictionary is a dictionary that can store multiple values per
key. An ordered multivalue dictionary is a multivalue dictionary that
retains the order of insertions and deletions.
Internally, items are stored in a doubly linked list, self._items. A
dictionary, self._map, is also maintained and stores an ordered list of
linked list node references, one for each value associated with that key.
Standard dict methods interact with the first value associated with a given
key. This means that omdict retains method parity with dict, and a dict
object can be replaced with an omdict object and all interaction will
behave identically. All dict methods that retain parity with omdict are:
get(), setdefault(), pop(), popitem(),
clear(), copy(), update(), fromkeys(), len()
__getitem__(), __setitem__(), __delitem__(), __contains__(),
items(), keys(), values(), iteritems(), iterkeys(), itervalues(),
Optional parameters have been added to some dict methods, but because the
added parameters are optional, existing use remains unaffected. An optional
<key> parameter has been added to these methods:
items(), values(), iteritems(), itervalues()
New methods have also been added to omdict. Methods with 'list' in their
name interact with lists of values, and methods with 'all' in their name
interact with all items in the dictionary, including multiple items with
the same key.
The new omdict methods are:
load(), size(), reverse(),
getlist(), add(), addlist(), set(), setlist(), setdefaultlist(),
poplist(), popvalue(), popvalues(), popitem(), poplistitem(),
allitems(), allkeys(), allvalues(), lists(), listitems(),
iterallitems(), iterallkeys(), iterallvalues(), iterlists(),
iterlistitems()
Explanations and examples of the new methods above can be found in the
function comments below and online at
https://github.com/gruns/orderedmultidict
Additional omdict information and documentation can also be found at the
above url.
"""
def __init__(self, *args, **kwargs):
# Doubly linked list of itemnodes. Each itemnode stores a key:value
# item.
self._items = itemlist()
# Ordered dictionary of keys and itemnode references. Each itemnode
# reference points to one of that keys values.
self._map = odict()
self.load(*args, **kwargs)
def load(self, *args, **kwargs):
"""
Clear all existing key:value items and import all key:value items from
<mapping>. If multiple values exist for the same key in <mapping>, they
are all be imported.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.load([(4,4), (4,44), (5,5)])
omd.allitems() == [(4,4), (4,44), (5,5)]
Returns: <self>.
"""
self.clear()
self.updateall(*args, **kwargs)
return self
def copy(self):
return self.__class__(self.allitems())
def clear(self):
self._map.clear()
self._items.clear()
def size(self):
"""
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.size() == 5
Returns: Total number of items, including multiple items with the same
key.
"""
return len(self._items)
@classmethod
def fromkeys(cls, iterable, value=None):
return cls([(key, value) for key in iterable])
def has_key(self, key):
return key in self
def update(self, *args, **kwargs):
self._update_updateall(True, *args, **kwargs)
def updateall(self, *args, **kwargs):
"""
Update this dictionary with the items from <mapping>, replacing
existing key:value items with shared keys before adding new key:value
items.
Example:
omd = omdict([(1,1), (2,2)])
omd.updateall([(2,'two'), (1,'one'), (2,222), (1,111)])
omd.allitems() == [(1, 'one'), (2, 'two'), (2, 222), (1, 111)]
Returns: <self>.
"""
self._update_updateall(False, *args, **kwargs)
return self
def _update_updateall(self, replace_at_most_one, *args, **kwargs):
# Bin the items in <args> and <kwargs> into <replacements> or
# <leftovers>. Items in <replacements> are new values to replace old
# values for a given key, and items in <leftovers> are new items to be
# added.
replacements, leftovers = dict(), []
for mapping in chain(args, [kwargs]):
self._bin_update_items(
self._items_iterator(mapping), replace_at_most_one,
replacements, leftovers)
# First, replace existing values for each key.
for key, values in six.iteritems(replacements):
self.setlist(key, values)
# Then, add the leftover items to the end of the list of all items.
for key, value in leftovers:
self.add(key, value)
def _bin_update_items(self, items, replace_at_most_one,
replacements, leftovers):
"""
<replacements and <leftovers> are modified directly, ala pass by
reference.
"""
for key, value in items:
# If there are existing items with key <key> that have yet to be
# marked for replacement, mark that item's value to be replaced by
# <value> by appending it to <replacements>.
if key in self and key not in replacements:
replacements[key] = [value]
elif (key in self and not replace_at_most_one and
len(replacements[key]) < len(self.values(key))):
replacements[key].append(value)
else:
if replace_at_most_one:
replacements[key] = [value]
else:
leftovers.append((key, value))
def _items_iterator(self, container):
cont = container
iterator = iter(cont)
if callable_attr(cont, 'iterallitems'):
iterator = cont.iterallitems()
elif callable_attr(cont, 'allitems'):
iterator = iter(cont.allitems())
elif callable_attr(cont, 'iteritems'):
iterator = cont.iteritems()
elif callable_attr(cont, 'items'):
iterator = iter(cont.items())
return iterator
def get(self, key, default=None):
if key in self:
return self._map[key][0].value
return default
def getlist(self, key, default=[]):
"""
Returns: The list of values for <key> if <key> is in the dictionary,
else <default>. If <default> is not provided, an empty list is
returned.
"""
if key in self:
return [node.value for node in self._map[key]]
return default
def setdefault(self, key, default=None):
if key in self:
return self[key]
self.add(key, default)
return default
def setdefaultlist(self, key, defaultlist=[None]):
"""
Similar to setdefault() except <defaultlist> is a list of values to set
for <key>. If <key> already exists, its existing list of values is
returned.
If <key> isn't a key and <defaultlist> is an empty list, [], no values
are added for <key> and <key> will not be added as a key.
Returns: List of <key>'s values if <key> exists in the dictionary,
otherwise <default>.
"""
if key in self:
return self.getlist(key)
self.addlist(key, defaultlist)
return defaultlist
def add(self, key, value=None):
"""
Add <value> to the list of values for <key>. If <key> is not in the
dictionary, then <value> is added as the sole value for <key>.
Example:
omd = omdict()
omd.add(1, 1) # omd.allitems() == [(1,1)]
omd.add(1, 11) # omd.allitems() == [(1,1), (1,11)]
omd.add(2, 2) # omd.allitems() == [(1,1), (1,11), (2,2)]
Returns: <self>.
"""
self._map.setdefault(key, [])
node = self._items.append(key, value)
self._map[key].append(node)
return self
def addlist(self, key, valuelist=[]):
"""
Add the values in <valuelist> to the list of values for <key>. If <key>
is not in the dictionary, the values in <valuelist> become the values
for <key>.
Example:
omd = omdict([(1,1)])
omd.addlist(1, [11, 111])
omd.allitems() == [(1, 1), (1, 11), (1, 111)]
omd.addlist(2, [2])
omd.allitems() == [(1, 1), (1, 11), (1, 111), (2, 2)]
Returns: <self>.
"""
for value in valuelist:
self.add(key, value)
return self
def set(self, key, value=None):
"""
Sets <key>'s value to <value>. Identical in function to __setitem__().
Returns: <self>.
"""
self[key] = value
return self
def setlist(self, key, values):
"""
Sets <key>'s list of values to <values>. Existing items with key <key>
are first replaced with new values from <values>. Any remaining old
items that haven't been replaced with new values are deleted, and any
new values from <values> that don't have corresponding items with <key>
to replace are appended to the end of the list of all items.
If values is an empty list, [], <key> is deleted, equivalent in action
to del self[<key>].
Example:
omd = omdict([(1,1), (2,2)])
omd.setlist(1, [11, 111])
omd.allitems() == [(1,11), (2,2), (1,111)]
omd = omdict([(1,1), (1,11), (2,2), (1,111)])
omd.setlist(1, [None])
omd.allitems() == [(1,None), (2,2)]
omd = omdict([(1,1), (1,11), (2,2), (1,111)])
omd.setlist(1, [])
omd.allitems() == [(2,2)]
Returns: <self>.
"""
if not values and key in self:
self.pop(key)
else:
it = zip_longest(
list(self._map.get(key, [])), values, fillvalue=_absent)
for node, value in it:
if node is not _absent and value is not _absent:
node.value = value
elif node is _absent:
self.add(key, value)
elif value is _absent:
self._map[key].remove(node)
self._items.removenode(node)
return self
def removevalues(self, key, values):
"""
Removes all <values> from the values of <key>. If <key> has no
remaining values after removevalues(), the key is popped.
Example:
omd = omdict([(1, 1), (1, 11), (1, 1), (1, 111)])
omd.removevalues(1, [1, 111])
omd.allitems() == [(1, 11)]
Returns: <self>.
"""
self.setlist(key, [v for v in self.getlist(key) if v not in values])
return self
def pop(self, key, default=_absent):
if key in self:
return self.poplist(key)[0]
elif key not in self._map and default is not _absent:
return default
raise KeyError(key)
def poplist(self, key, default=_absent):
"""
If <key> is in the dictionary, pop it and return its list of values. If
<key> is not in the dictionary, return <default>. KeyError is raised if
<default> is not provided and <key> is not in the dictionary.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.poplist(1) == [1, 11, 111]
omd.allitems() == [(2,2), (3,3)]
omd.poplist(2) == [2]
omd.allitems() == [(3,3)]
Raises: KeyError if <key> isn't in the dictionary and <default> isn't
provided.
Returns: List of <key>'s values.
"""
if key in self:
values = self.getlist(key)
del self._map[key]
for node, nodekey, nodevalue in self._items:
if nodekey == key:
self._items.removenode(node)
return values
elif key not in self._map and default is not _absent:
return default
raise KeyError(key)
def popvalue(self, key, value=_absent, default=_absent, last=True):
"""
If <value> is provided, pops the first or last (key,value) item in the
dictionary if <key> is in the dictionary.
If <value> is not provided, pops the first or last value for <key> if
<key> is in the dictionary.
If <key> no longer has any values after a popvalue() call, <key> is
removed from the dictionary. If <key> isn't in the dictionary and
<default> was provided, return default. KeyError is raised if <default>
is not provided and <key> is not in the dictionary. ValueError is
raised if <value> is provided but isn't a value for <key>.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3), (2,22)])
omd.popvalue(1) == 111
omd.allitems() == [(1,11), (1,111), (2,2), (3,3), (2,22)]
omd.popvalue(1, last=False) == 1
omd.allitems() == [(1,11), (2,2), (3,3), (2,22)]
omd.popvalue(2, 2) == 2
omd.allitems() == [(1,11), (3,3), (2,22)]
omd.popvalue(1, 11) == 11
omd.allitems() == [(3,3), (2,22)]
omd.popvalue('not a key', default='sup') == 'sup'
Params:
last: Boolean whether to return <key>'s first value (<last> is False)
or last value (<last> is True).
Raises:
KeyError if <key> isn't in the dictionary and <default> isn't
provided.
ValueError if <value> isn't a value for <key>.
Returns: The first or last of <key>'s values.
"""
def pop_node_with_index(key, index):
node = self._map[key].pop(index)
if not self._map[key]:
del self._map[key]
self._items.removenode(node)
return node
if key in self:
if value is not _absent:
if last:
pos = self.values(key)[::-1].index(value)
else:
pos = self.values(key).index(value)
if pos == -1:
raise ValueError(value)
else:
index = (len(self.values(key)) - 1 - pos) if last else pos
return pop_node_with_index(key, index).value
else:
return pop_node_with_index(key, -1 if last else 0).value
elif key not in self._map and default is not _absent:
return default
raise KeyError(key)
def popitem(self, fromall=False, last=True):
"""
Pop and return a key:value item.
If <fromall> is False, items()[0] is popped if <last> is False or
items()[-1] is popped if <last> is True. All remaining items with the
same key are removed.
If <fromall> is True, allitems()[0] is popped if <last> is False or
allitems()[-1] is popped if <last> is True. Any remaining items with
the same key remain.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.popitem() == (3,3)
omd.popitem(fromall=False, last=False) == (1,1)
omd.popitem(fromall=False, last=False) == (2,2)
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.popitem(fromall=True, last=False) == (1,1)
omd.popitem(fromall=True, last=False) == (1,11)
omd.popitem(fromall=True, last=True) == (3,3)
omd.popitem(fromall=True, last=False) == (1,111)
Params:
fromall: Whether to pop an item from items() (<fromall> is True) or
allitems() (<fromall> is False).
last: Boolean whether to pop the first item or last item of items()
or allitems().
Raises: KeyError if the dictionary is empty.
Returns: The first or last item from item() or allitem().
"""
if not self._items:
raise KeyError('popitem(): %s is empty' % self.__class__.__name__)
if fromall:
node = self._items[-1 if last else 0]
key = node.key
return key, self.popvalue(key, last=last)
else:
key = list(self._map.keys())[-1 if last else 0]
return key, self.pop(key)
def poplistitem(self, last=True):
"""
Pop and return a key:valuelist item comprised of a key and that key's
list of values. If <last> is False, a key:valuelist item comprised of
keys()[0] and its list of values is popped and returned. If <last> is
True, a key:valuelist item comprised of keys()[-1] and its list of
values is popped and returned.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.poplistitem(last=True) == (3,[3])
omd.poplistitem(last=False) == (1,[1,11,111])
Params:
last: Boolean whether to pop the first or last key and its associated
list of values.
Raises: KeyError if the dictionary is empty.
Returns: A two-tuple comprised of the first or last key and its
associated list of values.
"""
if not self._items:
s = 'poplistitem(): %s is empty' % self.__class__.__name__
raise KeyError(s)
key = self.keys()[-1 if last else 0]
return key, self.poplist(key)
def items(self, key=_absent):
"""
Raises: KeyError if <key> is provided and not in the dictionary.
Returns: List created from iteritems(<key>). Only items with key <key>
are returned if <key> is provided and is a dictionary key.
"""
return list(self.iteritems(key))
def keys(self):
return list(self.iterkeys())
def values(self, key=_absent):
"""
Raises: KeyError if <key> is provided and not in the dictionary.
Returns: List created from itervalues(<key>).If <key> is provided and
is a dictionary key, only values of items with key <key> are
returned.
"""
if key is not _absent and key in self._map:
return self.getlist(key)
return list(self.itervalues())
def lists(self):
"""
Returns: List created from iterlists().
"""
return list(self.iterlists())
def listitems(self):
"""
Returns: List created from iterlistitems().
"""
return list(self.iterlistitems())
def iteritems(self, key=_absent):
"""
Parity with dict.iteritems() except the optional <key> parameter has
been added. If <key> is provided, only items with the provided key are
iterated over. KeyError is raised if <key> is provided and not in the
dictionary.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.iteritems(1) -> (1,1) -> (1,11) -> (1,111)
omd.iteritems() -> (1,1) -> (2,2) -> (3,3)
Raises: KeyError if <key> is provided and not in the dictionary.
Returns: An iterator over the items() of the dictionary, or only items
with the key <key> if <key> is provided.
"""
if key is not _absent:
if key in self:
items = [(node.key, node.value) for node in self._map[key]]
return iter(items)
raise KeyError(key)
items = six.iteritems(self._map)
return iter((key, nodes[0].value) for (key, nodes) in items)
def iterkeys(self):
return six.iterkeys(self._map)
def itervalues(self, key=_absent):
"""
Parity with dict.itervalues() except the optional <key> parameter has
been added. If <key> is provided, only values from items with the
provided key are iterated over. KeyError is raised if <key> is provided
and not in the dictionary.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.itervalues(1) -> 1 -> 11 -> 111
omd.itervalues() -> 1 -> 11 -> 111 -> 2 -> 3
Raises: KeyError if <key> is provided and isn't in the dictionary.
Returns: An iterator over the values() of the dictionary, or only the
values of key <key> if <key> is provided.
"""
if key is not _absent:
if key in self:
return iter([node.value for node in self._map[key]])
raise KeyError(key)
return iter([nodes[0].value for nodes in six.itervalues(self._map)])
def allitems(self, key=_absent):
'''
Raises: KeyError if <key> is provided and not in the dictionary.
Returns: List created from iterallitems(<key>).
'''
return list(self.iterallitems(key))
def allkeys(self):
'''
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.allkeys() == [1,1,1,2,3]
Returns: List created from iterallkeys().
'''
return list(self.iterallkeys())
def allvalues(self, key=_absent):
'''
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.allvalues() == [1,11,111,2,3]
omd.allvalues(1) == [1,11,111]
Raises: KeyError if <key> is provided and not in the dictionary.
Returns: List created from iterallvalues(<key>).
'''
return list(self.iterallvalues(key))
def iterallitems(self, key=_absent):
'''
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.iterallitems() == (1,1) -> (1,11) -> (1,111) -> (2,2) -> (3,3)
omd.iterallitems(1) == (1,1) -> (1,11) -> (1,111)
Raises: KeyError if <key> is provided and not in the dictionary.
Returns: An iterator over every item in the diciontary. If <key> is
provided, only items with the key <key> are iterated over.
'''
if key is not _absent:
# Raises KeyError if <key> is not in self._map.
return self.iteritems(key)
return self._items.iteritems()
def iterallkeys(self):
'''
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.iterallkeys() == 1 -> 1 -> 1 -> 2 -> 3
Returns: An iterator over the keys of every item in the dictionary.
'''
return self._items.iterkeys()
def iterallvalues(self, key=_absent):
'''
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.iterallvalues() == 1 -> 11 -> 111 -> 2 -> 3
Returns: An iterator over the values of every item in the dictionary.
'''
if key is not _absent:
if key in self:
return iter(self.getlist(key))
raise KeyError(key)
return self._items.itervalues()
def iterlists(self):
'''
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.iterlists() -> [1,11,111] -> [2] -> [3]
Returns: An iterator over the list comprised of the lists of values for
each key.
'''
return map(lambda key: self.getlist(key), self)
def iterlistitems(self):
"""
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.iterlistitems() -> (1,[1,11,111]) -> (2,[2]) -> (3,[3])
Returns: An iterator over the list of key:valuelist items.
"""
return map(lambda key: (key, self.getlist(key)), self)
def reverse(self):
"""
Reverse the order of all items in the dictionary.
Example:
omd = omdict([(1,1), (1,11), (1,111), (2,2), (3,3)])
omd.reverse()
omd.allitems() == [(3,3), (2,2), (1,111), (1,11), (1,1)]
Returns: <self>.
"""
for key in six.iterkeys(self._map):
self._map[key].reverse()
self._items.reverse()
return self
def __eq__(self, other):
if callable_attr(other, 'iterallitems'):
myiter, otheriter = self.iterallitems(), other.iterallitems()
for i1, i2 in zip_longest(myiter, otheriter, fillvalue=_absent):
if i1 != i2 or i1 is _absent or i2 is _absent:
return False
elif not hasattr(other, '__len__') or not hasattr(other, items_attr):
return False
# Ignore order so we can compare ordered omdicts with unordered dicts.
else:
if len(self) != len(other):
return False
for key, value in six.iteritems(other):
if self.get(key, _absent) != value:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def __len__(self):
return len(self._map)
def __iter__(self):
for key in self.iterkeys():
yield key
def __contains__(self, key):
return key in self._map
def __getitem__(self, key):
if key in self:
return self.get(key)
raise KeyError(key)
def __setitem__(self, key, value):
self.setlist(key, [value])
def __delitem__(self, key):
return self.pop(key)
def __nonzero__(self):
return bool(self._map)
def __str__(self):
return '{%s}' % ', '.join(
map(lambda p: '%r: %r' % (p[0], p[1]), self.iterallitems()))
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, self.allitems())
+74 -57
View File
@@ -3,7 +3,7 @@ r'''Parse strings using a specification based on the Python format() syntax.
``parse()`` is the opposite of ``format()``
The module is set up to only export ``parse()``, ``search()``, ``findall()``,
and ``with_pattern()`` when ``import *`` is used:
and ``with_pattern()`` when ``import \*`` is used:
>>> from parse import *
@@ -78,9 +78,11 @@ Some simple parse() format string examples:
{'item': 'hand grenade'}
>>> print(r['item'])
hand grenade
>>> 'item' in r
True
Dotted names and indexes are possible though the application must make
additional sense of the result:
Note that `in` only works if you have named fields. Dotted names and indexes
are possible though the application must make additional sense of the result:
>>> r = parse("Mmm, {food.type}, I love it!", "Mmm, spam, I love it!")
>>> print(r)
@@ -132,38 +134,39 @@ The differences between `parse()` and `format()` are:
===== =========================================== ========
Type Characters Matched Output
===== =========================================== ========
w Letters and underscore str
W Non-letter and underscore str
s Whitespace str
S Non-whitespace str
d Digits (effectively integer numbers) int
D Non-digit str
n Numbers with thousands separators (, or .) int
% Percentage (converted to value/100.0) float
f Fixed-point numbers float
F Decimal numbers Decimal
e Floating-point numbers with exponent float
l Letters (ASCII) str
w Letters, numbers and underscore str
W Not letters, numbers and underscore str
s Whitespace str
S Non-whitespace str
d Digits (effectively integer numbers) int
D Non-digit str
n Numbers with thousands separators (, or .) int
% Percentage (converted to value/100.0) float
f Fixed-point numbers float
F Decimal numbers Decimal
e Floating-point numbers with exponent float
e.g. 1.1e-10, NAN (all case insensitive)
g General number format (either d, f or e) float
b Binary numbers int
o Octal numbers int
x Hexadecimal numbers (lower and upper case) int
ti ISO 8601 format date/time datetime
g General number format (either d, f or e) float
b Binary numbers int
o Octal numbers int
x Hexadecimal numbers (lower and upper case) int
ti ISO 8601 format date/time datetime
e.g. 1972-01-20T10:21:36Z ("T" and "Z"
optional)
te RFC2822 e-mail format date/time datetime
te RFC2822 e-mail format date/time datetime
e.g. Mon, 20 Jan 1972 10:21:36 +1000
tg Global (day/month) format date/time datetime
tg Global (day/month) format date/time datetime
e.g. 20/1/1972 10:21:36 AM +1:00
ta US (month/day) format date/time datetime
ta US (month/day) format date/time datetime
e.g. 1/20/1972 10:21:36 PM +10:30
tc ctime() format date/time datetime
tc ctime() format date/time datetime
e.g. Sun Sep 16 01:03:52 1973
th HTTP log format date/time datetime
th HTTP log format date/time datetime
e.g. 21/Nov/2011:00:07:11 +0000
ts Linux system log format date/time datetime
ts Linux system log format date/time datetime
e.g. Nov 9 03:37:44
tt Time time
tt Time time
e.g. 10:21:36 PM -5:30
===== =========================================== ========
@@ -342,6 +345,13 @@ the pattern, the actual match represents the shortest successful match for
**Version history (in brief)**:
- 1.11.1 Revert having unicode char in docstring, it breaks Bamboo builds(?!)
- 1.11.0 Implement `__contains__` for Result instances.
- 1.10.0 Introduce a "letters" matcher, since "w" matches numbers
also.
- 1.9.1 Fix deprecation warnings around backslashes in regex strings
(thanks Mickael Schoentgen). Also fix some documentation formatting
issues.
- 1.9.0 We now honor precision and width specifiers when parsing numbers
and strings, allowing parsing of concatenated elements of fixed width
(thanks Julia Signell)
@@ -400,12 +410,12 @@ the pattern, the actual match represents the shortest successful match for
and removed the restriction on mixing fixed-position and named fields
- 1.0.0 initial release
This code is copyright 2012-2017 Richard Jones <richard@python.org>
This code is copyright 2012-2019 Richard Jones <richard@python.org>
See the end of the source file for the license of use.
'''
from __future__ import absolute_import
__version__ = '1.9.0'
__version__ = '1.11.1'
# yes, I now have two problems
import re
@@ -530,9 +540,9 @@ MONTHS_MAP = dict(
Nov=11, November=11,
Dec=12, December=12
)
DAYS_PAT = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
MONTHS_PAT = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'
ALL_MONTHS_PAT = '(%s)' % '|'.join(MONTHS_MAP)
DAYS_PAT = r'(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
MONTHS_PAT = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'
ALL_MONTHS_PAT = r'(%s)' % '|'.join(MONTHS_MAP)
TIME_PAT = r'(\d{1,2}:\d{1,2}(:\d{1,2}(\.\d+)?)?)'
AM_PAT = r'(\s+[AP]M)'
TZ_PAT = r'(\s+[-+]\d\d?:?\d\d)'
@@ -550,11 +560,11 @@ def date_convert(string, match, ymd=None, mdy=None, dmy=None,
m=groups[mm]
d=groups[dd]
elif ymd is not None:
y, m, d = re.split('[-/\s]', groups[ymd])
y, m, d = re.split(r'[-/\s]', groups[ymd])
elif mdy is not None:
m, d, y = re.split('[-/\s]', groups[mdy])
m, d, y = re.split(r'[-/\s]', groups[mdy])
elif dmy is not None:
d, m, y = re.split('[-/\s]', groups[dmy])
d, m, y = re.split(r'[-/\s]', groups[dmy])
elif d_m_y is not None:
d, m, y = d_m_y
d = groups[d]
@@ -636,10 +646,10 @@ class RepeatedNameError(ValueError):
# note: {} are handled separately
# note: I don't use r'' here because Sublime Text 2 syntax highlight has a fit
REGEX_SAFETY = re.compile('([?\\\\.[\]()*+\^$!\|])')
REGEX_SAFETY = re.compile(r'([?\\\\.[\]()*+\^$!\|])')
# allowed field types
ALLOWED_TYPES = set(list('nbox%fFegwWdDsS') +
ALLOWED_TYPES = set(list('nbox%fFegwWdDsSl') +
['t' + c for c in 'ieahgcts'])
@@ -745,7 +755,7 @@ class Parser(object):
@property
def _match_re(self):
if self.__match_re is None:
expression = '^%s$' % self._expression
expression = r'^%s$' % self._expression
try:
self.__match_re = re.compile(expression, self._re_flags)
except AssertionError:
@@ -923,16 +933,16 @@ class Parser(object):
name, self._name_types[name]))
group = self._name_to_group_map[name]
# match previously-seen value
return '(?P=%s)' % group
return r'(?P=%s)' % group
else:
group = self._to_group_name(name)
self._name_types[name] = format
self._named_fields.append(group)
# this will become a group, which must not contain dots
wrap = '(?P<%s>%%s)' % group
wrap = r'(?P<%s>%%s)' % group
else:
self._fixed_fields.append(self._group_index)
wrap = '(%s)'
wrap = r'(%s)'
if ':' in field:
format = field[1:]
group = self._group_index
@@ -940,7 +950,7 @@ class Parser(object):
# simplest case: no type specifier ({} or {name})
if not format:
self._group_index += 1
return wrap % '.+?'
return wrap % r'.+?'
# decode the format specification
format = extract_format(format, self._extra_types)
@@ -960,19 +970,19 @@ class Parser(object):
return type_converter(string)
self._type_conversions[group] = f
elif type == 'n':
s = '\d{1,3}([,.]\d{3})*'
s = r'\d{1,3}([,.]\d{3})*'
self._group_index += 1
self._type_conversions[group] = int_convert(10)
elif type == 'b':
s = '(0[bB])?[01]+'
s = r'(0[bB])?[01]+'
self._type_conversions[group] = int_convert(2)
self._group_index += 1
elif type == 'o':
s = '(0[oO])?[0-7]+'
s = r'(0[oO])?[0-7]+'
self._type_conversions[group] = int_convert(8)
self._group_index += 1
elif type == 'x':
s = '(0[xX])?[0-9a-fA-F]+'
s = r'(0[xX])?[0-9a-fA-F]+'
self._type_conversions[group] = int_convert(16)
self._group_index += 1
elif type == '%':
@@ -994,10 +1004,10 @@ class Parser(object):
self._type_conversions[group] = lambda s, m: float(s)
elif type == 'd':
if format.get('width'):
width = '{1,%s}' % int(format['width'])
width = r'{1,%s}' % int(format['width'])
else:
width = '+'
s = '\\d{w}|0[xX][0-9a-fA-F]{w}|0[bB][01]{w}|0[oO][0-7]{w}'.format(w=width)
s = r'\d{w}|0[xX][0-9a-fA-F]{w}|0[bB][01]{w}|0[oO][0-7]{w}'.format(w=width)
self._type_conversions[group] = int_convert(10)
elif type == 'ti':
s = r'(\d{4}-\d\d-\d\d)((\s+|T)%s)?(Z|\s*[-+]\d\d:?\d\d)?' % \
@@ -1055,18 +1065,19 @@ class Parser(object):
self._type_conversions[group] = partial(date_convert, mm=n+1, dd=n+3,
hms=n + 5)
self._group_index += 5
elif type == 'l':
s = r'[A-Za-z]+'
elif type:
s = r'\%s+' % type
elif format.get('precision'):
if format.get('width'):
s = '.{%s,%s}?' % (format['width'], format['precision'])
s = r'.{%s,%s}?' % (format['width'], format['precision'])
else:
s = '.{1,%s}?' % format['precision']
s = r'.{1,%s}?' % format['precision']
elif format.get('width'):
s = '.{%s,}?' % format['width']
s = r'.{%s,}?' % format['width']
else:
s = '.+?'
s = r'.+?'
align = format['align']
fill = format['fill']
@@ -1079,7 +1090,7 @@ class Parser(object):
# configurable fill defaulting to "0"
if not fill:
fill = '0'
s = '%s*' % fill + s
s = r'%s*' % fill + s
# allow numbers to be prefixed with a sign
s = r'[-+ ]?' + s
@@ -1101,7 +1112,7 @@ class Parser(object):
if not align:
align = '>'
if fill in '.\+?*[](){}^$':
if fill in r'.\+?*[](){}^$':
fill = '\\' + fill
# align "=" has been handled
@@ -1118,8 +1129,11 @@ class Parser(object):
class Result(object):
'''The result of a parse() or search().
Fixed results may be looked up using result[index]. Named results may be
looked up using result['name'].
Fixed results may be looked up using `result[index]`.
Named results may be looked up using `result['name']`.
Named results may be tested for existence using `'name' in result`.
'''
def __init__(self, fixed, named, spans):
self.fixed = fixed
@@ -1135,6 +1149,9 @@ class Result(object):
return '<%s %r %r>' % (self.__class__.__name__, self.fixed,
self.named)
def __contains__(self, name):
return name in self.named
class Match(object):
'''The result of a parse() or search() if no results are generated.
@@ -1295,7 +1312,7 @@ def compile(format, extra_types=None, case_sensitive=False):
return Parser(format, extra_types=extra_types)
# Copyright (c) 2012-2013 Richard Jones <richard@python.org>
# Copyright (c) 2012-2019 Richard Jones <richard@python.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
+10 -3
View File
@@ -22,7 +22,7 @@ import pkg_resources
# from graphviz import backend, Digraph
__version__ = '0.13.1'
__version__ = '0.13.2'
flatten = chain.from_iterable
@@ -127,6 +127,13 @@ def guess_version(pkg_key, default='?'):
return getattr(m, '__version__', default)
def frozen_req_from_dist(dist):
try:
return FrozenRequirement.from_dist(dist)
except TypeError:
return FrozenRequirement.from_dist(dist, [])
class Package(object):
"""Abstract class for wrappers around objects that pip returns.
@@ -154,7 +161,7 @@ class Package(object):
@staticmethod
def frozen_repr(obj):
fr = FrozenRequirement.from_dist(obj, [])
fr = frozen_req_from_dist(obj)
return str(fr).strip()
def __getattr__(self, key):
@@ -563,7 +570,7 @@ def _get_args():
def main():
args = _get_args()
pkgs = get_installed_distributions(local_only=args.local_only,
user_only=args.user_only)
user_only=args.user_only)
dist_index = build_dist_index(pkgs)
tree = construct_tree(dist_index)
+8 -5
View File
@@ -1,16 +1,19 @@
from __future__ import absolute_import, print_function
__version__ = '1.1.11'
# Add NullHandler to "pythonfinder" logger, because Python2's default root
# logger has no handler and warnings like this would be reported:
#
# > No handlers could be found for logger "pythonfinder.models.pyenv"
import logging
from .exceptions import InvalidPythonVersion
from .models import SystemPath, WindowsFinder
from .pythonfinder import Finder
__version__ = "1.2.0"
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
__all__ = ["Finder", "WindowsFinder", "SystemPath", "InvalidPythonVersion"]
from .pythonfinder import Finder
from .models import SystemPath, WindowsFinder
from .exceptions import InvalidPythonVersion
+1 -3
View File
@@ -13,9 +13,7 @@ from .pythonfinder import Finder
@click.command()
@click.option("--find", default=False, nargs=1, help="Find a specific python version.")
@click.option("--which", default=False, nargs=1, help="Run the which command.")
@click.option(
"--findall", is_flag=True, default=False, help="Find all python versions."
)
@click.option("--findall", is_flag=True, default=False, help="Find all python versions.")
@click.option(
"--version", is_flag=True, default=False, help="Display PythonFinder version."
)
+3
View File
@@ -36,6 +36,7 @@ else:
IGNORE_UNSUPPORTED = bool(os.environ.get("PYTHONFINDER_IGNORE_UNSUPPORTED", False))
MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
def get_shim_paths():
shim_paths = []
if ASDF_INSTALLED:
@@ -43,4 +44,6 @@ def get_shim_paths():
if PYENV_INSTALLED:
shim_paths.append(os.path.join(PYENV_ROOT, "shims"))
return [os.path.normpath(os.path.normcase(p)) for p in shim_paths]
SHIM_PATHS = get_shim_paths()
+19 -21
View File
@@ -3,23 +3,23 @@ from __future__ import absolute_import, unicode_literals
import abc
import operator
from collections import defaultdict
import attr
import six
from cached_property import cached_property
from vistir.compat import fs_str
from ..environment import MYPY_RUNNING
from ..exceptions import InvalidPythonVersion
from ..utils import (
KNOWN_EXTS, Sequence, expand_paths, looks_like_python,
path_is_known_executable
KNOWN_EXTS,
Sequence,
expand_paths,
looks_like_python,
path_is_known_executable,
)
if MYPY_RUNNING:
from .path import PathEntry
from .python import PythonVersion
@@ -48,7 +48,9 @@ class BasePath(object):
only_python = attr.ib(default=False) # type: bool
name = attr.ib(type=str)
_py_version = attr.ib(default=None) # type: Optional[PythonVersion]
_pythons = attr.ib(default=attr.Factory(defaultdict)) # type: DefaultDict[str, PathEntry]
_pythons = attr.ib(
default=attr.Factory(defaultdict)
) # type: DefaultDict[str, PathEntry]
def __str__(self):
# type: () -> str
@@ -197,6 +199,7 @@ class BasePath(object):
# type: () -> DefaultDict[Union[str, Path], PathEntry]
if not self._pythons:
from .path import PathEntry
self._pythons = defaultdict(PathEntry)
for python in self._iter_pythons():
python_path = python.path.as_posix() # type: ignore
@@ -241,17 +244,13 @@ class BasePath(object):
:rtype: List[:class:`~pythonfinder.models.PathEntry`]
"""
call_method = (
"find_all_python_versions" if self.is_dir else "find_python_version"
)
call_method = "find_all_python_versions" if self.is_dir else "find_python_version"
sub_finder = operator.methodcaller(
call_method, major, minor, patch, pre, dev, arch, name
)
if not self.is_dir:
return sub_finder(self)
unnested = [
sub_finder(path) for path in expand_paths(self)
]
unnested = [sub_finder(path) for path in expand_paths(self)]
version_sort = operator.attrgetter("as_python.version_sort")
unnested = [p for p in unnested if p is not None and p.as_python is not None]
paths = sorted(unnested, key=version_sort, reverse=True)
@@ -291,13 +290,13 @@ class BasePath(object):
matching_pythons = [
[entry, entry.as_python.version_sort]
for entry in self._iter_pythons()
if (entry is not None and entry.as_python is not None and
version_matcher(entry.py_version))
if (
entry is not None
and entry.as_python is not None
and version_matcher(entry.py_version)
)
]
results = sorted(matching_pythons,
key=operator.itemgetter(1, 0),
reverse=True,
)
results = sorted(matching_pythons, key=operator.itemgetter(1, 0), reverse=True)
return next(iter(r[0] for r in results if r is not None), None)
@@ -316,9 +315,8 @@ class BaseFinder(object):
raise NotImplementedError
@classmethod
def create(cls, # type: Type[BaseFinderType]
*args, # type: Any
**kwargs # type: Any
def create(
cls, *args, **kwargs # type: Type[BaseFinderType] # type: Any # type: Any
):
# type: (...) -> BaseFinderType
raise NotImplementedError
+87 -49
View File
@@ -5,60 +5,90 @@ import copy
import operator
import os
import sys
from collections import defaultdict
from itertools import chain
import attr
import six
from cached_property import cached_property
from vistir.compat import Path, fs_str
from .mixins import BaseFinder, BasePath
from .python import PythonVersion
from ..environment import (
ASDF_DATA_DIR, ASDF_INSTALLED, MYPY_RUNNING, PYENV_INSTALLED, PYENV_ROOT,
SHIM_PATHS
ASDF_DATA_DIR,
ASDF_INSTALLED,
MYPY_RUNNING,
PYENV_INSTALLED,
PYENV_ROOT,
SHIM_PATHS,
)
from ..exceptions import InvalidPythonVersion
from ..utils import (
Iterable, Sequence, ensure_path, expand_paths, filter_pythons, is_in_path,
looks_like_python, normalize_path, optional_instance_of,
parse_asdf_version_order, parse_pyenv_version_order,
path_is_known_executable, unnest
Iterable,
Sequence,
ensure_path,
expand_paths,
filter_pythons,
is_in_path,
looks_like_python,
normalize_path,
optional_instance_of,
parse_asdf_version_order,
parse_pyenv_version_order,
path_is_known_executable,
unnest,
)
from .mixins import BaseFinder, BasePath
from .python import PythonVersion
if MYPY_RUNNING:
from typing import (
Optional, Dict, DefaultDict, Iterator, List, Union, Tuple, Generator, Callable,
Type, Any, TypeVar
Optional,
Dict,
DefaultDict,
Iterator,
List,
Union,
Tuple,
Generator,
Callable,
Type,
Any,
TypeVar,
)
from .mixins import BaseFinder
from .python import PythonFinder
from .windows import WindowsFinder
FinderType = TypeVar('FinderType', BaseFinder, PythonFinder, WindowsFinder)
ChildType = Union[PythonFinder, PathEntry]
PathType = Union[PythonFinder, PathEntry]
FinderType = TypeVar("FinderType", BaseFinder, PythonFinder, WindowsFinder)
ChildType = Union[PythonFinder, "PathEntry"]
PathType = Union[PythonFinder, "PathEntry"]
@attr.s
class SystemPath(object):
global_search = attr.ib(default=True)
paths = attr.ib(default=attr.Factory(defaultdict)) # type: DefaultDict[str, Union[PythonFinder, PathEntry]]
paths = attr.ib(
default=attr.Factory(defaultdict)
) # type: DefaultDict[str, Union[PythonFinder, PathEntry]]
_executables = attr.ib(default=attr.Factory(list)) # type: List[PathEntry]
_python_executables = attr.ib(default=attr.Factory(dict)) # type: Dict[str, PathEntry]
_python_executables = attr.ib(
default=attr.Factory(dict)
) # type: Dict[str, PathEntry]
path_order = attr.ib(default=attr.Factory(list)) # type: List[str]
python_version_dict = attr.ib() # type: DefaultDict[Tuple, List[PythonVersion]]
only_python = attr.ib(default=False, type=bool)
pyenv_finder = attr.ib(default=None, validator=optional_instance_of("PythonFinder")) # type: Optional[PythonFinder]
pyenv_finder = attr.ib(
default=None, validator=optional_instance_of("PythonFinder")
) # type: Optional[PythonFinder]
asdf_finder = attr.ib(default=None) # type: Optional[PythonFinder]
system = attr.ib(default=False, type=bool)
_version_dict = attr.ib(default=attr.Factory(defaultdict)) # type: DefaultDict[Tuple, List[PathEntry]]
_version_dict = attr.ib(
default=attr.Factory(defaultdict)
) # type: DefaultDict[Tuple, List[PathEntry]]
ignore_unsupported = attr.ib(default=False, type=bool)
__finders = attr.ib(default=attr.Factory(dict)) # type: Dict[str, Union[WindowsFinder, PythonFinder]]
__finders = attr.ib(
default=attr.Factory(dict)
) # type: Dict[str, Union[WindowsFinder, PythonFinder]]
def _register_finder(self, finder_name, finder):
# type: (str, Union[WindowsFinder, PythonFinder]) -> None
@@ -117,7 +147,9 @@ class SystemPath(object):
@cached_property
def version_dict(self):
# type: () -> DefaultDict[Tuple, List[PathEntry]]
self._version_dict = defaultdict(list) # type: DefaultDict[Tuple, List[PathEntry]]
self._version_dict = defaultdict(
list
) # type: DefaultDict[Tuple, List[PathEntry]]
for finder_name, finder in self.__finders.items():
for version, entry in finder.versions.items():
if finder_name == "windows":
@@ -171,9 +203,7 @@ class SystemPath(object):
reversed_paths = reversed(self.path_order)
paths = [normalize_path(p) for p in reversed_paths]
normalized_target = normalize_path(path)
last_instance = next(
iter(p for p in paths if normalized_target in p), None
)
last_instance = next(iter(p for p in paths if normalized_target in p), None)
if last_instance is None:
raise ValueError("No instance found on path for target: {0!s}".format(path))
path_index = self.path_order.index(last_instance)
@@ -190,19 +220,14 @@ class SystemPath(object):
else:
before_path = self.path_order[: start_idx + 1]
after_path = self.path_order[start_idx + 2 :]
self.path_order = (
before_path + [p.as_posix() for p in paths] + after_path
)
self.path_order = before_path + [p.as_posix() for p in paths] + after_path
def _remove_path(self, path):
# type: (str) -> None
path_copy = [p for p in reversed(self.path_order[:])]
new_order = []
target = normalize_path(path)
path_map = {
normalize_path(pth): pth
for pth in self.paths.keys()
}
path_map = {normalize_path(pth): pth for pth in self.paths.keys()}
if target in path_map:
del self.paths[path_map[target]]
for current_path in path_copy:
@@ -215,10 +240,14 @@ class SystemPath(object):
def _setup_asdf(self):
# type: () -> None
from .python import PythonFinder
os_path = os.environ["PATH"].split(os.pathsep)
self.asdf_finder = PythonFinder.create(
root=ASDF_DATA_DIR, ignore_unsupported=True,
sort_function=parse_asdf_version_order, version_glob_path="installs/python/*")
root=ASDF_DATA_DIR,
ignore_unsupported=True,
sort_function=parse_asdf_version_order,
version_glob_path="installs/python/*",
)
asdf_index = None
try:
asdf_index = self._get_last_instance(ASDF_DATA_DIR)
@@ -253,7 +282,9 @@ class SystemPath(object):
# TODO: This is called 'reload', should we load a new finder for the first
# time here? lets just skip that for now to avoid unallowed finders
pass
if (finder_name == "pyenv" and not PYENV_INSTALLED) or (finder_name == "asdf" and not ASDF_INSTALLED):
if (finder_name == "pyenv" and not PYENV_INSTALLED) or (
finder_name == "asdf" and not ASDF_INSTALLED
):
# Don't allow loading of finders that aren't explicitly 'installed' as it were
pass
setattr(self, finder_attr, None)
@@ -264,10 +295,15 @@ class SystemPath(object):
def _setup_pyenv(self):
# type: () -> None
from .python import PythonFinder
os_path = os.environ["PATH"].split(os.pathsep)
self.pyenv_finder = PythonFinder.create(
root=PYENV_ROOT, sort_function=parse_pyenv_version_order, version_glob_path="versions/*", ignore_unsupported=self.ignore_unsupported)
root=PYENV_ROOT,
sort_function=parse_pyenv_version_order,
version_glob_path="versions/*",
ignore_unsupported=self.ignore_unsupported,
)
pyenv_index = None
try:
pyenv_index = self._get_last_instance(PYENV_ROOT)
@@ -469,8 +505,7 @@ class SystemPath(object):
name = "{0!s}".format(major)
major = None
sub_finder = operator.methodcaller(
"find_python_version",
major, minor, patch, pre, dev, arch, name,
"find_python_version", major, minor, patch, pre, dev, arch, name
)
alternate_sub_finder = None
if name and not (minor or patch or pre or dev or arch or major):
@@ -517,7 +552,9 @@ class SystemPath(object):
:rtype: :class:`pythonfinder.models.SystemPath`
"""
path_entries = defaultdict(PathEntry) # type: DefaultDict[str, Union[PythonFinder, PathEntry]]
path_entries = defaultdict(
PathEntry
) # type: DefaultDict[str, Union[PythonFinder, PathEntry]]
paths = [] # type: List[str]
if ignore_unsupported:
os.environ["PYTHONFINDER_IGNORE_UNSUPPORTED"] = fs_str("1")
@@ -567,6 +604,7 @@ class PathEntry(BasePath):
def _gen_children(self):
# type: () -> Iterator
from ..environment import get_shim_paths
shim_paths = get_shim_paths()
pass_name = self.name != self.path.name
pass_args = {"is_root": False, "only_python": self.only_python}
@@ -602,7 +640,6 @@ class PathEntry(BasePath):
self._children = children
return self._children
@classmethod
def create(cls, path, is_root=False, only_python=False, pythons=None, name=None):
# type: (Union[str, Path], bool, bool, Dict[str, PythonVersion], Optional[str]) -> PathEntry
@@ -622,16 +659,18 @@ class PathEntry(BasePath):
if not name:
guessed_name = True
name = target.name
creation_args = {"path": target, "is_root": is_root, "only_python": only_python, "name": name}
creation_args = {
"path": target,
"is_root": is_root,
"only_python": only_python,
"name": name,
}
if pythons:
creation_args["pythons"] = pythons
_new = cls(**creation_args)
if pythons and only_python:
children = {}
child_creation_args = {
"is_root": False,
"only_python": only_python
}
child_creation_args = {"is_root": False, "only_python": only_python}
if not guessed_name:
child_creation_args["name"] = _new.name # type: ignore
for pth, python in pythons.items():
@@ -639,9 +678,7 @@ class PathEntry(BasePath):
continue
pth = ensure_path(pth)
children[pth.as_posix()] = PathEntry( # type: ignore
py_version=python,
path=pth,
**child_creation_args
py_version=python, path=pth, **child_creation_args
)
_new._children = children
return _new
@@ -658,6 +695,7 @@ class VersionPath(SystemPath):
Generates the version listings for it"""
from .path import PathEntry
path = ensure_path(path)
path_entries = defaultdict(PathEntry)
bin_ = "{base}/bin"
+68 -34
View File
@@ -6,29 +6,44 @@ import logging
import operator
import platform
import sys
from collections import defaultdict
import attr
import six
from packaging.version import Version
from vistir.compat import Path, lru_cache
from .mixins import BaseFinder, BasePath
from ..environment import ASDF_DATA_DIR, MYPY_RUNNING, PYENV_ROOT, SYSTEM_ARCH
from ..exceptions import InvalidPythonVersion
from ..utils import (
RE_MATCHER, _filter_none, ensure_path, get_python_version, is_in_path,
looks_like_python, optional_instance_of, parse_asdf_version_order,
parse_pyenv_version_order, parse_python_version, unnest
RE_MATCHER,
_filter_none,
ensure_path,
get_python_version,
is_in_path,
looks_like_python,
optional_instance_of,
parse_asdf_version_order,
parse_pyenv_version_order,
parse_python_version,
unnest,
)
from .mixins import BaseFinder, BasePath
if MYPY_RUNNING:
from typing import (
DefaultDict, Optional, Callable, Generator, Any, Union, Tuple, List, Dict, Type,
TypeVar, Iterator
DefaultDict,
Optional,
Callable,
Generator,
Any,
Union,
Tuple,
List,
Dict,
Type,
TypeVar,
Iterator,
)
from .path import PathEntry
from .._vendor.pep514tools.environment import Environment
@@ -68,8 +83,7 @@ class PythonFinder(BaseFinder, BasePath):
def expanded_paths(self):
# type: () -> Generator
return (
path for path in unnest(p for p in self.versions.values())
if path is not None
path for path in unnest(p for p in self.versions.values()) if path is not None
)
@property
@@ -85,15 +99,20 @@ class PythonFinder(BaseFinder, BasePath):
def get_version_order(self):
# type: () -> List[Path]
version_paths = [
p for p in self.root.glob(self.version_glob_path)
p
for p in self.root.glob(self.version_glob_path)
if not (p.parent.name == "envs" or p.name == "envs")
]
versions = {v.name: v for v in version_paths}
version_order = [] # type: List[Path]
if self.is_pyenv:
version_order = [versions[v] for v in parse_pyenv_version_order() if v in versions]
version_order = [
versions[v] for v in parse_pyenv_version_order() if v in versions
]
elif self.is_asdf:
version_order = [versions[v] for v in parse_asdf_version_order() if v in versions]
version_order = [
versions[v] for v in parse_asdf_version_order() if v in versions
]
for version in version_order:
version_paths.remove(version)
if version_order:
@@ -118,12 +137,12 @@ class PythonFinder(BaseFinder, BasePath):
def _iter_version_bases(self):
# type: () -> Iterator[Tuple[Path, PathEntry]]
from .path import PathEntry
for p in self.get_version_order():
bin_dir = self.get_bin_dir(p)
if bin_dir.exists() and bin_dir.is_dir():
entry = PathEntry.create(
path=bin_dir.absolute(), only_python=False, name=p.name,
is_root=True
path=bin_dir.absolute(), only_python=False, name=p.name, is_root=True
)
self.roots[p] = entry
yield (p, entry)
@@ -146,8 +165,11 @@ class PythonFinder(BaseFinder, BasePath):
except Exception:
if not self.ignore_unsupported:
raise
logger.warning("Unsupported Python version %r, ignoring...",
base_path.name, exc_info=True)
logger.warning(
"Unsupported Python version %r, ignoring...",
base_path.name,
exc_info=True,
)
continue
if version is not None:
version_tuple = (
@@ -190,6 +212,7 @@ class PythonFinder(BaseFinder, BasePath):
# type: () -> DefaultDict[str, PathEntry]
if not self._pythons:
from .path import PathEntry
self._pythons = defaultdict(PathEntry) # type: DefaultDict[str, PathEntry]
for python in self._iter_pythons():
python_path = python.path.as_posix() # type: ignore
@@ -206,13 +229,20 @@ class PythonFinder(BaseFinder, BasePath):
return self.pythons
@classmethod
def create(cls, root, sort_function, version_glob_path=None, ignore_unsupported=True): # type: ignore
def create(
cls, root, sort_function, version_glob_path=None, ignore_unsupported=True
): # type: ignore
# type: (Type[PythonFinder], str, Callable, Optional[str], bool) -> PythonFinder
root = ensure_path(root)
if not version_glob_path:
version_glob_path = "versions/*"
return cls(root=root, path=root, ignore_unsupported=ignore_unsupported, # type: ignore
sort_function=sort_function, version_glob_path=version_glob_path)
return cls(
root=root,
path=root,
ignore_unsupported=ignore_unsupported, # type: ignore
sort_function=sort_function,
version_glob_path=version_glob_path,
)
def find_all_python_versions(
self,
@@ -239,9 +269,7 @@ class PythonFinder(BaseFinder, BasePath):
:rtype: List[:class:`~pythonfinder.models.PathEntry`]
"""
call_method = (
"find_all_python_versions" if self.is_dir else "find_python_version"
)
call_method = "find_all_python_versions" if self.is_dir else "find_python_version"
sub_finder = operator.methodcaller(
call_method, major, minor, patch, pre, dev, arch, name
)
@@ -251,13 +279,12 @@ class PythonFinder(BaseFinder, BasePath):
for _, base in self._iter_version_bases()
]
else:
pythons = [
sub_finder(path) for path in self.paths
]
pythons = [sub_finder(path) for path in self.paths]
pythons = [p for p in pythons if p and p.is_python and p.as_python is not None]
version_sort = operator.attrgetter("as_python.version_sort")
paths = [
p for p in sorted(list(pythons), key=version_sort, reverse=True)
p
for p in sorted(list(pythons), key=version_sort, reverse=True)
if p is not None
]
return paths
@@ -292,7 +319,8 @@ class PythonFinder(BaseFinder, BasePath):
version_sort = operator.attrgetter("as_python.version_sort")
unnested = [sub_finder(self.roots[path]) for path in self.roots]
unnested = [
p for p in unnested
p
for p in unnested
if p is not None and p.is_python and p.as_python is not None
]
paths = sorted(list(unnested), key=version_sort, reverse=True)
@@ -527,6 +555,7 @@ class PythonVersion(object):
if not isinstance(path, PathEntry):
path = PathEntry.create(path, is_root=False, only_python=True, name=name)
from ..environment import IGNORE_UNSUPPORTED
ignore_unsupported = ignore_unsupported or IGNORE_UNSUPPORTED
path_name = getattr(path, "name", path.path.name) # str
if not path.is_python:
@@ -540,7 +569,10 @@ class PythonVersion(object):
if instance_dict.get("minor") is None and looks_like_python(path.path.name):
instance_dict = cls.parse_executable(path.path.absolute().as_posix())
if not isinstance(instance_dict.get("version"), Version) and not ignore_unsupported:
if (
not isinstance(instance_dict.get("version"), Version)
and not ignore_unsupported
):
raise ValueError("Not a valid python path: %s" % path)
if instance_dict.get("patch") is None:
instance_dict = cls.parse_executable(path.path.absolute().as_posix())
@@ -596,7 +628,7 @@ class PythonVersion(object):
launcher_entry.info, "sys_architecture", SYSTEM_ARCH
),
"executable": exe_path,
"name": name
"name": name,
}
)
py_version = cls.create(**creation_dict)
@@ -616,7 +648,9 @@ class PythonVersion(object):
@attr.s
class VersionMap(object):
versions = attr.ib(factory=defaultdict) # type: DefaultDict[Tuple[int, Optional[int], Optional[int], bool, bool, bool], List[PathEntry]]
versions = attr.ib(
factory=defaultdict
) # type: DefaultDict[Tuple[int, Optional[int], Optional[int], bool, bool, bool], List[PathEntry]]
def add_entry(self, entry):
# type: (...) -> None
@@ -634,8 +668,8 @@ class VersionMap(object):
self.versions[version] = entries
else:
current_entries = {
p.path for p in
self.versions[version] # type: ignore
p.path
for p in self.versions[version] # type: ignore
if version in self.versions
}
new_entries = {p.path for p in entries}
+26 -23
View File
@@ -2,22 +2,21 @@
from __future__ import absolute_import, print_function
import operator
from collections import defaultdict
import attr
from ..environment import MYPY_RUNNING
from ..exceptions import InvalidPythonVersion
from ..utils import ensure_path
from .mixins import BaseFinder
from .path import PathEntry
from .python import PythonVersion, VersionMap
from ..environment import MYPY_RUNNING
from ..exceptions import InvalidPythonVersion
from ..utils import ensure_path
if MYPY_RUNNING:
from typing import DefaultDict, Tuple, List, Optional, Union, TypeVar, Type, Any
FinderType = TypeVar('FinderType')
FinderType = TypeVar("FinderType")
@attr.s
@@ -41,9 +40,7 @@ class WindowsFinder(BaseFinder):
version_matcher = operator.methodcaller(
"matches", major, minor, patch, pre, dev, arch, python_name=name
)
pythons = [
py for py in self.version_list if version_matcher(py)
]
pythons = [py for py in self.version_list if version_matcher(py)]
version_sort = operator.attrgetter("version_sort")
return [c.comes_from for c in sorted(pythons, key=version_sort, reverse=True)]
@@ -58,15 +55,20 @@ class WindowsFinder(BaseFinder):
name=None, # type: Optional[str]
):
# type: (...) -> Optional[PathEntry]
return next(iter(v for v in self.find_all_python_versions(
major=major,
minor=minor,
patch=patch,
pre=pre,
dev=dev,
arch=arch,
name=name,
)), None
return next(
iter(
v
for v in self.find_all_python_versions(
major=major,
minor=minor,
patch=patch,
pre=pre,
dev=dev,
arch=arch,
name=name,
)
),
None,
)
@_versions.default
@@ -92,13 +94,14 @@ class WindowsFinder(BaseFinder):
if py_version is None:
continue
self.version_list.append(py_version)
python_path = py_version.comes_from.path if py_version.comes_from else py_version.executable
python_path = (
py_version.comes_from.path
if py_version.comes_from
else py_version.executable
)
python_kwargs = {python_path: py_version} if python_path is not None else {}
base_dir = PathEntry.create(
path,
is_root=True,
only_python=True,
pythons=python_kwargs,
path, is_root=True, only_python=True, pythons=python_kwargs
)
versions[py_version.version_tuple[:5]] = base_dir
self.paths.append(base_dir)
+31 -18
View File
@@ -5,16 +5,14 @@ import operator
import os
import six
from click import secho
from vistir.compat import lru_cache
from . import environment
from .exceptions import InvalidPythonVersion
from .models import path
from .models import path as pyfinder_path
from .utils import Iterable, filter_pythons, version_re
if environment.MYPY_RUNNING:
from typing import Optional, Dict, Any, Union, List, Iterator
from .models.path import Path, PathEntry
@@ -34,7 +32,9 @@ class Finder(object):
*path* and *system*.
"""
def __init__(self, path=None, system=False, global_search=True, ignore_unsupported=True):
def __init__(
self, path=None, system=False, global_search=True, ignore_unsupported=True
):
# type: (Optional[str], bool, bool, bool) -> None
"""Create a new :class:`~pythonfinder.pythonfinder.Finder` instance.
@@ -68,9 +68,11 @@ class Finder(object):
def create_system_path(self):
# type: () -> SystemPath
return path.SystemPath.create(
path=self.path_prepend, system=self.system, global_search=self.global_search,
ignore_unsupported=self.ignore_unsupported
return pyfinder_path.SystemPath.create(
path=self.path_prepend,
system=self.system,
global_search=self.global_search,
ignore_unsupported=self.ignore_unsupported,
)
def reload_system_path(self):
@@ -84,7 +86,7 @@ class Finder(object):
if self._system_path is not None:
self._system_path.clear_caches()
self._system_path = None
six.moves.reload_module(path)
six.moves.reload_module(pyfinder_path)
self._system_path = self.create_system_path()
def rehash(self):
@@ -136,12 +138,13 @@ class Finder(object):
"""
from .models import PythonVersion
minor = int(minor) if minor is not None else minor
patch = int(patch) if patch is not None else patch
version_dict = {
"minor": minor,
"patch": patch
"patch": patch,
} # type: Dict[str, Union[str, int, Any]]
if (
@@ -177,7 +180,9 @@ class Finder(object):
if "." in major and all(part.isdigit() for part in major.split(".")[:2]):
match = version_re.match(major)
version_dict = match.groupdict()
version_dict["is_prerelease"] = bool(version_dict.get("prerel", False))
version_dict["is_prerelease"] = bool(
version_dict.get("prerel", False)
)
version_dict["is_devrelease"] = bool(version_dict.get("dev", False))
else:
version_dict = {
@@ -186,7 +191,7 @@ class Finder(object):
"patch": patch,
"pre": pre,
"dev": dev,
"arch": arch
"arch": arch,
}
if version_dict.get("minor") is not None:
minor = int(version_dict["minor"])
@@ -198,10 +203,18 @@ class Finder(object):
pre = bool(_pre) if _pre is not None else pre
_dev = version_dict.get("is_devrelease", dev)
dev = bool(_dev) if _dev is not None else dev
arch = version_dict.get("architecture", None) if arch is None else arch # type: ignore
arch = (
version_dict.get("architecture", None) if arch is None else arch
) # type: ignore
if os.name == "nt" and self.windows_finder is not None:
match = self.windows_finder.find_python_version(
major=major, minor=minor, patch=patch, pre=pre, dev=dev, arch=arch, name=name
major=major,
minor=minor,
patch=patch,
pre=pre,
dev=dev,
arch=arch,
name=name,
)
if match:
return match
@@ -218,10 +231,10 @@ class Finder(object):
python_version_dict = getattr(self.system_path, "python_version_dict")
if python_version_dict:
paths = (
path
for version in python_version_dict.values()
for path in version
if path is not None and path.as_python
path
for version in python_version_dict.values()
for path in version
if path is not None and path.as_python
)
path_list = sorted(paths, key=version_sort, reverse=True)
return path_list
@@ -229,7 +242,7 @@ class Finder(object):
major=major, minor=minor, patch=patch, pre=pre, dev=dev, arch=arch, name=name
)
if not isinstance(versions, Iterable):
versions = [versions,]
versions = [versions]
path_list = sorted(versions, key=version_sort, reverse=True)
path_map = {} # type: Dict[str, PathEntry]
for path in path_list:
+61 -34
View File
@@ -5,23 +5,26 @@ import io
import itertools
import os
import re
from fnmatch import fnmatch
import attr
import six
import vistir
from packaging.version import LegacyVersion, Version
from .environment import MYPY_RUNNING, PYENV_ROOT
from .exceptions import InvalidPythonVersion
six.add_move(six.MovedAttribute("Iterable", "collections", "collections.abc")) # type: ignore # noqa
six.add_move(six.MovedAttribute("Sequence", "collections", "collections.abc")) # type: ignore # noqa
from six.moves import Iterable # type: ignore # noqa
from six.moves import Sequence # type: ignore # noqa
six.add_move(
six.MovedAttribute("Iterable", "collections", "collections.abc")
) # type: ignore # noqa
six.add_move(
six.MovedAttribute("Sequence", "collections", "collections.abc")
) # type: ignore # noqa
# fmt: off
from six.moves import Iterable # type: ignore # noqa # isort:skip
from six.moves import Sequence # type: ignore # noqa # isort:skip
# fmt: on
try:
from functools import lru_cache
@@ -29,27 +32,42 @@ except ImportError:
from backports.functools_lru_cache import lru_cache # type: ignore # noqa
if MYPY_RUNNING:
from typing import (
Any, Union, List, Callable, Iterable, Set, Tuple, Dict, Optional, Iterator
)
from typing import Any, Union, List, Callable, Set, Tuple, Dict, Optional, Iterator
from attr.validators import _OptionalValidator # type: ignore
from .models.path import PathEntry
version_re = re.compile(r"(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>(?<=\.)[0-9]+))?\.?"
r"(?:(?P<prerel>[abc]|rc|dev)(?:(?P<prerelversion>\d+(?:\.\d+)*))?)"
r"?(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?")
version_re = re.compile(
r"(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>(?<=\.)[0-9]+))?\.?"
r"(?:(?P<prerel>[abc]|rc|dev)(?:(?P<prerelversion>\d+(?:\.\d+)*))?)"
r"?(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?"
)
PYTHON_IMPLEMENTATIONS = (
"python", "ironpython", "jython", "pypy", "anaconda", "miniconda",
"stackless", "activepython", "micropython"
"python",
"ironpython",
"jython",
"pypy",
"anaconda",
"miniconda",
"stackless",
"activepython",
"micropython",
)
RE_MATCHER = re.compile(
r"(({0})(?:\d?(?:\.\d[cpm]{{0,3}}))?(?:-?[\d\.]+)*[^z])".format(
"|".join(PYTHON_IMPLEMENTATIONS)
)
)
RE_MATCHER = re.compile(r"(({0})(?:\d?(?:\.\d[cpm]{{0,3}}))?(?:-?[\d\.]+)*[^z])".format(
"|".join(PYTHON_IMPLEMENTATIONS)
))
RULES_BASE = [
"*{0}", "*{0}?", "*{0}?.?", "*{0}?.?m", "{0}?-?.?", "{0}?-?.?.?", "{0}?.?-?.?.?"
"*{0}",
"*{0}?",
"*{0}?.?",
"*{0}?.?m",
"{0}?-?.?",
"{0}?-?.?.?",
"{0}?.?-?.?.?",
]
RULES = [rule.format(impl) for impl in PYTHON_IMPLEMENTATIONS for rule in RULES_BASE]
@@ -61,10 +79,7 @@ KNOWN_EXTS = KNOWN_EXTS | set(
MATCH_RULES = []
for rule in RULES:
MATCH_RULES.extend(
[
"{0}.{1}".format(rule, ext) if ext else "{0}".format(rule)
for ext in KNOWN_EXTS
]
["{0}.{1}".format(rule, ext) if ext else "{0}".format(rule) for ext in KNOWN_EXTS]
)
@@ -74,8 +89,14 @@ def get_python_version(path):
"""Get python version string using subprocess from a given path."""
version_cmd = [path, "-c", "import sys; print(sys.version.split()[0])"]
try:
c = vistir.misc.run(version_cmd, block=True, nospin=True, return_object=True,
combine_stderr=False, write_to_stdout=False)
c = vistir.misc.run(
version_cmd,
block=True,
nospin=True,
return_object=True,
combine_stderr=False,
write_to_stdout=False,
)
except OSError:
raise InvalidPythonVersion("%s is not a valid python path" % path)
if not c.out:
@@ -87,6 +108,7 @@ def get_python_version(path):
def parse_python_version(version_str):
# type: (str) -> Dict[str, Union[str, int, Version]]
from packaging.version import parse as parse_version
is_debug = False
if version_str.endswith("-debug"):
is_debug = True
@@ -127,7 +149,7 @@ def parse_python_version(version_str):
"is_prerelease": is_prerelease,
"is_devrelease": is_devrelease,
"is_debug": is_debug,
"version": version
"version": version,
}
@@ -237,9 +259,11 @@ def _filter_none(k, v):
# TODO: Reimplement in vistir
def normalize_path(path):
# type: (str) -> str
return os.path.normpath(os.path.normcase(
os.path.abspath(os.path.expandvars(os.path.expanduser(str(path))))
))
return os.path.normpath(
os.path.normcase(
os.path.abspath(os.path.expandvars(os.path.expanduser(str(path))))
)
)
@lru_cache(maxsize=1024)
@@ -290,9 +314,10 @@ def parse_asdf_version_order(filename=".tool-versions"):
if os.path.exists(version_order_file) and os.path.isfile(version_order_file):
with io.open(version_order_file, encoding="utf-8") as fh:
contents = fh.read()
python_section = next(iter(
line for line in contents.splitlines() if line.startswith("python")
), None)
python_section = next(
iter(line for line in contents.splitlines() if line.startswith("python")),
None,
)
if python_section:
# python_key, _, versions
_, _, versions = python_section.partition(" ")
@@ -317,8 +342,10 @@ def expand_paths(path, only_python=True):
:rtype: Iterator[PathEntry]
"""
if path is not None and (isinstance(path, Sequence) and
not getattr(path.__class__, "__name__", "") == "PathEntry"):
if path is not None and (
isinstance(path, Sequence)
and not getattr(path.__class__, "__name__", "") == "PathEntry"
):
for p in unnest(path):
if p is None:
continue
+16
View File
@@ -0,0 +1,16 @@
No-notice MIT License
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.
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.
+8 -4
View File
@@ -1,17 +1,21 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
__version__ = '1.4.1.dev0'
import logging
import warnings
from vistir.compat import ResourceWarning
from .models.lockfile import Lockfile
from .models.pipfile import Pipfile
from .models.requirements import Requirement
__version__ = "1.4.2"
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
warnings.filterwarnings("ignore", category=ResourceWarning)
from .models.requirements import Requirement
from .models.lockfile import Lockfile
from .models.pipfile import Pipfile
__all__ = ["Lockfile", "Pipfile", "Requirement"]
+17 -1
View File
@@ -20,6 +20,7 @@ from vistir.contextmanagers import cd, temp_environ
from vistir.misc import partialclass
from vistir.path import create_tracked_tempdir
from ..environment import MYPY_RUNNING
from ..utils import prepare_pip_source_args, _ensure_dir
from .cache import CACHE_DIR, DependencyCache
from .utils import (
@@ -29,6 +30,17 @@ from .utils import (
)
if MYPY_RUNNING:
from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text, Set, AnyStr
from pip_shims.shims import InstallRequirement, InstallationCandidate, PackageFinder, Command
from packaging.requirements import Requirement as PackagingRequirement
TRequirement = TypeVar("TRequirement")
RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement)
MarkerType = TypeVar('MarkerType', covariant=True, bound=Marker)
STRING_TYPE = Union[str, bytes, Text]
S = TypeVar("S", bytes, str, Text)
PKGS_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "pkgs"))
WHEEL_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "wheels"))
@@ -43,6 +55,7 @@ def _get_filtered_versions(ireq, versions, prereleases):
def find_all_matches(finder, ireq, pre=False):
# type: (PackageFinder, InstallRequirement, bool) -> List[InstallationCandidate]
"""Find all matching dependencies using the supplied finder and the
given ireq.
@@ -65,6 +78,7 @@ def find_all_matches(finder, ireq, pre=False):
def get_pip_command():
# type: () -> Command
# Use pip's parser for pip.conf management and defaults.
# General options (find_links, index_url, extra_index_url, trusted_host,
# and pre) are defered to pip.
@@ -89,7 +103,7 @@ def get_pip_command():
@attr.s
class AbstractDependency(object):
name = attr.ib()
name = attr.ib() # type: STRING_TYPE
specifiers = attr.ib()
markers = attr.ib()
candidates = attr.ib()
@@ -284,6 +298,7 @@ def get_abstract_dependencies(reqs, sources=None, parent=None):
def get_dependencies(ireq, sources=None, parent=None):
# type: (Union[InstallRequirement, InstallationCandidate], Optional[List[Dict[S, Union[S, bool]]]], Optional[AbstractDependency]) -> Set[S, ...]
"""Get all dependencies for a given install requirement.
:param ireq: A single InstallRequirement
@@ -556,6 +571,7 @@ def get_pip_options(args=[], sources=None, pip_command=None):
def get_finder(sources=None, pip_command=None, pip_options=None):
# type: (List[Dict[S, Union[S, bool]]], Optional[Command], Any) -> PackageFinder
"""Get a package finder for looking up candidates to install
:param sources: A list of pipfile-formatted sources, defaults to None
File diff suppressed because it is too large Load Diff
+214 -127
View File
@@ -19,22 +19,21 @@ from distlib.wheel import Wheel
from packaging.markers import Marker
from six.moves import configparser
from six.moves.urllib.parse import unquote, urlparse, urlunparse
from vistir.compat import Iterable, Path, lru_cache
from vistir.contextmanagers import cd, temp_path
from vistir.misc import run
from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p, rmtree
from ..environment import MYPY_RUNNING
from ..exceptions import RequirementError
from .utils import (
get_default_pyproject_backend,
get_name_variants,
get_pyproject,
init_requirement,
split_vcs_method_from_uri,
strip_extras_markers_from_requirement,
get_default_pyproject_backend
)
from ..environment import MYPY_RUNNING
from ..exceptions import RequirementError
try:
from setuptools.dist import distutils
@@ -49,15 +48,34 @@ except ImportError:
if MYPY_RUNNING:
from typing import Any, Dict, List, Generator, Optional, Union, Tuple, TypeVar, Text, Set
from typing import (
Any,
Dict,
List,
Generator,
Optional,
Union,
Tuple,
TypeVar,
Text,
Set,
AnyStr,
)
from pip_shims.shims import InstallRequirement, PackageFinder
from pkg_resources import (
PathMetadata, DistInfoDistribution, Requirement as PkgResourcesRequirement
PathMetadata,
DistInfoDistribution,
Requirement as PkgResourcesRequirement,
)
from packaging.requirements import Requirement as PackagingRequirement
TRequirement = TypeVar("TRequirement")
RequirementType = TypeVar('RequirementType', covariant=True, bound=PackagingRequirement)
MarkerType = TypeVar('MarkerType', covariant=True, bound=Marker)
RequirementType = TypeVar(
"RequirementType", covariant=True, bound=PackagingRequirement
)
MarkerType = TypeVar("MarkerType", covariant=True, bound=Marker)
STRING_TYPE = Union[str, bytes, Text]
S = TypeVar("S", bytes, str, Text)
CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv"))
@@ -69,22 +87,43 @@ _setup_distribution = None
def pep517_subprocess_runner(cmd, cwd=None, extra_environ=None):
# type: (List[Text], Optional[Text], Optional[Dict[Text, Text]]) -> None
# type: (List[AnyStr], Optional[AnyStr], Optional[Dict[AnyStr, AnyStr]]) -> None
"""The default method of calling the wrapper subprocess."""
env = os.environ.copy()
if extra_environ:
env.update(extra_environ)
run(cmd, cwd=cwd, env=env, block=True, combine_stderr=True, return_object=False,
write_to_stdout=False, nospin=True)
run(
cmd,
cwd=cwd,
env=env,
block=True,
combine_stderr=True,
return_object=False,
write_to_stdout=False,
nospin=True,
)
class BuildEnv(pep517.envbuild.BuildEnvironment):
def pip_install(self, reqs):
cmd = [sys.executable, '-m', 'pip', 'install', '--ignore-installed', '--prefix',
self.path] + list(reqs)
run(cmd, block=True, combine_stderr=True, return_object=False,
write_to_stdout=False, nospin=True)
cmd = [
sys.executable,
"-m",
"pip",
"install",
"--ignore-installed",
"--prefix",
self.path,
] + list(reqs)
run(
cmd,
block=True,
combine_stderr=True,
return_object=False,
write_to_stdout=False,
nospin=True,
)
class HookCaller(pep517.wrappers.Pep517HookCaller):
@@ -135,7 +174,7 @@ def build_pep517(source_dir, build_dir, config_settings=None, dist_type="wheel")
@ensure_mkdir_p(mode=0o775)
def _get_src_dir(root):
# type: (Text) -> Text
# type: (AnyStr) -> AnyStr
src = os.environ.get("PIP_SRC")
if src:
return src
@@ -152,8 +191,9 @@ def _get_src_dir(root):
@lru_cache()
def ensure_reqs(reqs):
# type: (List[Union[Text, PkgResourcesRequirement]]) -> List[PkgResourcesRequirement]
# type: (List[Union[S, PkgResourcesRequirement]]) -> List[PkgResourcesRequirement]
import pkg_resources
if not isinstance(reqs, Iterable):
raise TypeError("Expecting an Iterable, got %r" % reqs)
new_reqs = []
@@ -167,19 +207,21 @@ def ensure_reqs(reqs):
return new_reqs
def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, editable=False):
# type: (Optional[InstallRequirement], Optional[Text], Optional[Text], bool) -> Dict[Text, Text]
download_dir = os.path.join(CACHE_DIR, "pkgs") # type: Text
def _prepare_wheel_building_kwargs(
ireq=None, src_root=None, src_dir=None, editable=False
):
# type: (Optional[InstallRequirement], Optional[AnyStr], Optional[AnyStr], bool) -> Dict[AnyStr, AnyStr]
download_dir = os.path.join(CACHE_DIR, "pkgs") # type: STRING_TYPE
mkdir_p(download_dir)
wheel_download_dir = os.path.join(CACHE_DIR, "wheels") # type: Text
wheel_download_dir = os.path.join(CACHE_DIR, "wheels") # type: STRING_TYPE
mkdir_p(wheel_download_dir)
if src_dir is None:
if editable and src_root is not None:
src_dir = src_root
elif ireq is None and src_root is not None:
src_dir = _get_src_dir(root=src_root) # type: Text
src_dir = _get_src_dir(root=src_root) # type: STRING_TYPE
elif ireq is not None and ireq.editable and src_root is not None:
src_dir = _get_src_dir(root=src_root)
else:
@@ -199,7 +241,7 @@ def _prepare_wheel_building_kwargs(ireq=None, src_root=None, src_dir=None, edita
def iter_metadata(path, pkg_name=None, metadata_type="egg-info"):
# type: (Text, Optional[Text], Text) -> Generator
# type: (AnyStr, Optional[AnyStr], AnyStr) -> Generator
if pkg_name is not None:
pkg_variants = get_name_variants(pkg_name)
non_matching_dirs = []
@@ -212,14 +254,17 @@ def iter_metadata(path, pkg_name=None, metadata_type="egg-info"):
elif not entry.name.endswith(metadata_type):
non_matching_dirs.append(entry)
for entry in non_matching_dirs:
for dir_entry in iter_metadata(entry.path, pkg_name=pkg_name, metadata_type=metadata_type):
for dir_entry in iter_metadata(
entry.path, pkg_name=pkg_name, metadata_type=metadata_type
):
yield dir_entry
def find_egginfo(target, pkg_name=None):
# type: (Text, Optional[Text]) -> Generator
# type: (AnyStr, Optional[AnyStr]) -> Generator
egg_dirs = (
egg_dir for egg_dir in iter_metadata(target, pkg_name=pkg_name)
egg_dir
for egg_dir in iter_metadata(target, pkg_name=pkg_name)
if egg_dir is not None
)
if pkg_name:
@@ -230,9 +275,12 @@ def find_egginfo(target, pkg_name=None):
def find_distinfo(target, pkg_name=None):
# type: (Text, Optional[Text]) -> Generator
# type: (AnyStr, Optional[AnyStr]) -> Generator
dist_dirs = (
dist_dir for dist_dir in iter_metadata(target, pkg_name=pkg_name, metadata_type="dist-info")
dist_dir
for dist_dir in iter_metadata(
target, pkg_name=pkg_name, metadata_type="dist-info"
)
if dist_dir is not None
)
if pkg_name:
@@ -243,7 +291,7 @@ def find_distinfo(target, pkg_name=None):
def get_metadata(path, pkg_name=None, metadata_type=None):
# type: (Text, Optional[Text], Optional[Text]) -> Dict[Text, Union[Text, List[RequirementType], Dict[Text, RequirementType]]]
# type: (S, Optional[S], Optional[S]) -> Dict[S, Union[S, List[RequirementType], Dict[S, RequirementType]]]
metadata_dirs = []
wheel_allowed = metadata_type == "wheel" or metadata_type is None
egg_allowed = metadata_type == "egg" or metadata_type is None
@@ -258,6 +306,7 @@ def get_metadata(path, pkg_name=None, metadata_type=None):
base_dir = None
if matched_dir is not None:
import pkg_resources
metadata_dir = os.path.abspath(matched_dir.path)
base_dir = os.path.dirname(metadata_dir)
dist = None
@@ -279,7 +328,7 @@ def get_metadata(path, pkg_name=None, metadata_type=None):
@lru_cache()
def get_extra_name_from_marker(marker):
# type: (MarkerType) -> Optional[Text]
# type: (MarkerType) -> Optional[S]
if not marker:
raise ValueError("Invalid value for marker: {0!r}".format(marker))
if not getattr(marker, "_markers", None):
@@ -291,7 +340,7 @@ def get_extra_name_from_marker(marker):
def get_metadata_from_wheel(wheel_path):
# type: (Text) -> Dict[Any, Any]
# type: (S) -> Dict[Any, Any]
if not isinstance(wheel_path, six.string_types):
raise TypeError("Expected string instance, received {0!r}".format(wheel_path))
try:
@@ -318,16 +367,11 @@ def get_metadata_from_wheel(wheel_path):
extras[extra].append(parsed_req)
else:
requires.append(parsed_req)
return {
"name": name,
"version": version,
"requires": requires,
"extras": extras
}
return {"name": name, "version": version, "requires": requires, "extras": extras}
def get_metadata_from_dist(dist):
# type: (Union[PathMetadata, DistInfoDistribution]) -> Dict[Text, Union[Text, List[RequirementType], Dict[Text, RequirementType]]]
# type: (Union[PathMetadata, DistInfoDistribution]) -> Dict[S, Union[S, List[RequirementType], Dict[S, RequirementType]]]
try:
requires = dist.requires()
except Exception:
@@ -360,31 +404,33 @@ def get_metadata_from_dist(dist):
"name": dist.project_name,
"version": dist.version,
"requires": requires,
"extras": extras
"extras": extras,
}
@attr.s(slots=True, frozen=True)
class BaseRequirement(object):
name = attr.ib(default="", cmp=True) # type: Text
requirement = attr.ib(default=None, cmp=True) # type: Optional[PkgResourcesRequirement]
name = attr.ib(default="", cmp=True) # type: STRING_TYPE
requirement = attr.ib(
default=None, cmp=True
) # type: Optional[PkgResourcesRequirement]
def __str__(self):
# type: () -> Text
# type: () -> S
return "{0}".format(str(self.requirement))
def as_dict(self):
# type: () -> Dict[Text, Optional[PkgResourcesRequirement]]
# type: () -> Dict[S, Optional[PkgResourcesRequirement]]
return {self.name: self.requirement}
def as_tuple(self):
# type: () -> Tuple[Text, Optional[PkgResourcesRequirement]]
# type: () -> Tuple[S, Optional[PkgResourcesRequirement]]
return (self.name, self.requirement)
@classmethod
@lru_cache()
def from_string(cls, line):
# type: (Text) -> BaseRequirement
# type: (S) -> BaseRequirement
line = line.strip()
req = init_requirement(line)
return cls.from_req(req)
@@ -406,54 +452,63 @@ class BaseRequirement(object):
@attr.s(slots=True, frozen=True)
class Extra(object):
name = attr.ib(default=None, cmp=True) # type: Text
name = attr.ib(default=None, cmp=True) # type: STRING_TYPE
requirements = attr.ib(factory=frozenset, cmp=True, type=frozenset)
def __str__(self):
# type: () -> Text
return "{0}: {{{1}}}".format(self.section, ", ".join([r.name for r in self.requirements]))
# type: () -> S
return "{0}: {{{1}}}".format(
self.section, ", ".join([r.name for r in self.requirements])
)
def add(self, req):
# type: (BaseRequirement) -> None
if req not in self.requirements:
return attr.evolve(self, requirements=frozenset(set(self.requirements).add(req)))
return attr.evolve(
self, requirements=frozenset(set(self.requirements).add(req))
)
return self
def as_dict(self):
# type: () -> Dict[Text, Tuple[RequirementType, ...]]
# type: () -> Dict[S, Tuple[RequirementType, ...]]
return {self.name: tuple([r.requirement for r in self.requirements])}
@attr.s(slots=True, cmp=True, hash=True)
class SetupInfo(object):
name = attr.ib(default=None, cmp=True) # type: Text
base_dir = attr.ib(default=None, cmp=True, hash=False) # type: Text
version = attr.ib(default=None, cmp=True) # type: Text
name = attr.ib(default=None, cmp=True) # type: STRING_TYPE
base_dir = attr.ib(default=None, cmp=True, hash=False) # type: STRING_TYPE
version = attr.ib(default=None, cmp=True) # type: STRING_TYPE
_requirements = attr.ib(type=frozenset, factory=frozenset, cmp=True, hash=True)
build_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True)
build_backend = attr.ib(cmp=True) # type: Text
build_backend = attr.ib(cmp=True) # type: STRING_TYPE
setup_requires = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True)
python_requires = attr.ib(type=packaging.specifiers.SpecifierSet, default=None, cmp=True)
python_requires = attr.ib(
type=packaging.specifiers.SpecifierSet, default=None, cmp=True
)
_extras_requirements = attr.ib(type=tuple, default=attr.Factory(tuple), cmp=True)
setup_cfg = attr.ib(type=Path, default=None, cmp=True, hash=False)
setup_py = attr.ib(type=Path, default=None, cmp=True, hash=False)
pyproject = attr.ib(type=Path, default=None, cmp=True, hash=False)
ireq = attr.ib(default=None, cmp=True, hash=False) # type: Optional[InstallRequirement]
ireq = attr.ib(
default=None, cmp=True, hash=False
) # type: Optional[InstallRequirement]
extra_kwargs = attr.ib(default=attr.Factory(dict), type=dict, cmp=False, hash=False)
metadata = attr.ib(default=None) # type: Optional[Tuple[Text]]
metadata = attr.ib(default=None) # type: Optional[Tuple[STRING_TYPE]]
@build_backend.default
def get_build_backend(self):
# type: () -> S
return get_default_pyproject_backend()
@property
def requires(self):
# type: () -> Dict[Text, RequirementType]
# type: () -> Dict[S, RequirementType]
return {req.name: req.requirement for req in self._requirements}
@property
def extras(self):
# type: () -> Dict[Text, Optional[Any]]
# type: () -> Dict[S, Optional[Any]]
extras_dict = {}
extras = set(self._extras_requirements)
for section, deps in extras:
@@ -465,7 +520,7 @@ class SetupInfo(object):
@classmethod
def get_setup_cfg(cls, setup_cfg_path):
# type: (Text) -> Dict[Text, Union[Text, None, Set[BaseRequirement], List[Text], Tuple[Text, Tuple[BaseRequirement]]]]
# type: (S) -> Dict[S, Union[S, None, Set[BaseRequirement], List[S], Tuple[S, Tuple[BaseRequirement]]]]
if os.path.exists(setup_cfg_path):
default_opts = {
"metadata": {"name": "", "version": ""},
@@ -486,35 +541,40 @@ class SetupInfo(object):
results["version"] = parser.get("metadata", "version")
install_requires = set() # type: Set[BaseRequirement]
if parser.has_option("options", "install_requires"):
install_requires = set([
BaseRequirement.from_string(dep)
for dep in parser.get("options", "install_requires").split("\n")
if dep
])
install_requires = set(
[
BaseRequirement.from_string(dep)
for dep in parser.get("options", "install_requires").split("\n")
if dep
]
)
results["install_requires"] = install_requires
if parser.has_option("options", "python_requires"):
results["python_requires"] = parser.get("options", "python_requires")
if parser.has_option("options", "build_requires"):
results["build_requires"] = parser.get("options", "build_requires")
extras_require = ()
extras = []
if "options.extras_require" in parser.sections():
extras_require = tuple([
(section, tuple([
BaseRequirement.from_string(dep)
for dep in parser.get(
"options.extras_require", section
).split("\n")
if dep
]))
for section in parser.options("options.extras_require")
if section not in ["options", "metadata"]
])
results["extras_require"] = extras_require
extras_require_section = parser.options("options.extras_require")
for section in extras_require_section:
if section in ["options", "metadata"]:
continue
section_contents = parser.get("options.extras_require", section)
section_list = section_contents.split("\n")
section_extras = []
for extra_name in section_list:
if not extra_name or extra_name.startswith("#"):
continue
section_extras.append(BaseRequirement.from_string(extra_name))
if section_extras:
extras.append(tuple([section, tuple(section_extras)]))
results["extras_require"] = tuple(extras)
return results
@property
def egg_base(self):
base = None # type: Optional[Text]
# type: () -> S
base = None # type: Optional[STRING_TYPE]
if self.setup_py.exists():
base = self.setup_py.parent
elif self.pyproject.exists():
@@ -541,21 +601,30 @@ class SetupInfo(object):
self.version = parsed.get("version")
build_requires = parsed.get("build_requires", [])
if self.build_requires:
self.build_requires = tuple(set(self.build_requires) | set(build_requires))
self.build_requires = tuple(
set(self.build_requires) | set(build_requires)
)
self._requirements = frozenset(
set(self._requirements) | set(parsed["install_requires"])
)
if self.python_requires is None:
self.python_requires = parsed.get("python_requires")
if not self._extras_requirements:
self._extras_requirements = (parsed["extras_require"])
self._extras_requirements = parsed["extras_require"]
else:
self._extras_requirements = self._extras_requirements + parsed["extras_require"]
self._extras_requirements = (
self._extras_requirements + parsed["extras_require"]
)
if self.ireq is not None and self.ireq.extras:
for extra in self.ireq.extras:
if extra in self.extras:
extras_tuple = tuple([BaseRequirement.from_req(req) for req in self.extras[extra]])
extras_tuple = tuple(
[BaseRequirement.from_req(req) for req in self.extras[extra]]
)
self._extras_requirements += ((extra, extras_tuple),)
self._requirements = frozenset(
set(self._requirements) | set(list(extras_tuple))
)
def run_setup(self):
# type: () -> None
@@ -578,16 +647,22 @@ class SetupInfo(object):
_setup_stop_after = "run"
sys.argv[0] = script_name
sys.argv[1:] = args
with open(script_name, 'rb') as f:
with open(script_name, "rb") as f:
if sys.version_info < (3, 5):
exec(f.read(), g, local_dict)
else:
exec(f.read(), g)
# We couldn't import everything needed to run setup
except NameError:
python = os.environ.get('PIP_PYTHON_PATH', sys.executable)
out, _ = run([python, "setup.py"] + args, cwd=target_cwd, block=True,
combine_stderr=False, return_object=False, nospin=True)
python = os.environ.get("PIP_PYTHON_PATH", sys.executable)
out, _ = run(
[python, "setup.py"] + args,
cwd=target_cwd,
block=True,
combine_stderr=False,
return_object=False,
nospin=True,
)
finally:
_setup_stop_after = None
sys.argv = save_argv
@@ -615,15 +690,13 @@ class SetupInfo(object):
if not install_requires:
install_requires = dist.install_requires
if install_requires and not self.requires:
requirements = set([
BaseRequirement.from_req(req) for req in install_requires
])
requirements = set(
[BaseRequirement.from_req(req) for req in install_requires]
)
if getattr(self.ireq, "extras", None):
for extra in self.ireq.extras:
requirements |= set(list(self.extras.get(extra, [])))
self._requirements = frozenset(
set(self._requirements) | requirements
)
self._requirements = frozenset(set(self._requirements) | requirements)
if dist.setup_requires and not self.setup_requires:
self.setup_requires = tuple(dist.setup_requires)
if not self.version:
@@ -637,38 +710,48 @@ class SetupInfo(object):
return config
def build_wheel(self):
# type: () -> Text
# type: () -> S
if not self.pyproject.exists():
build_requires = ", ".join(['"{0}"'.format(r) for r in self.build_requires])
self.pyproject.write_text(u"""
self.pyproject.write_text(
u"""
[build-system]
requires = [{0}]
build-backend = "{1}"
""".format(build_requires, self.build_backend).strip())
""".format(
build_requires, self.build_backend
).strip()
)
return build_pep517(
self.base_dir, self.extra_kwargs["build_dir"],
self.base_dir,
self.extra_kwargs["build_dir"],
config_settings=self.pep517_config,
dist_type="wheel"
dist_type="wheel",
)
# noinspection PyPackageRequirements
def build_sdist(self):
# type: () -> Text
# type: () -> S
if not self.pyproject.exists():
build_requires = ", ".join(['"{0}"'.format(r) for r in self.build_requires])
self.pyproject.write_text(u"""
self.pyproject.write_text(
u"""
[build-system]
requires = [{0}]
build-backend = "{1}"
""".format(build_requires, self.build_backend).strip())
""".format(
build_requires, self.build_backend
).strip()
)
return build_pep517(
self.base_dir, self.extra_kwargs["build_dir"],
self.base_dir,
self.extra_kwargs["build_dir"],
config_settings=self.pep517_config,
dist_type="sdist"
dist_type="sdist",
)
def build(self):
# type: () -> Optional[Text]
# type: () -> None
dist_path = None
try:
dist_path = self.build_wheel()
@@ -686,9 +769,10 @@ build-backend = "{1}"
self.get_egg_metadata()
if not self.metadata or not self.name:
self.run_setup()
return None
def reload(self):
# type: () -> Dict[Text, Any]
# type: () -> Dict[S, Any]
"""
Wipe existing distribution info metadata for rebuilding.
"""
@@ -700,23 +784,28 @@ build-backend = "{1}"
self.get_info()
def get_metadata_from_wheel(self, wheel_path):
# type: (Text) -> Dict[Any, Any]
# type: (S) -> Dict[Any, Any]
metadata_dict = get_metadata_from_wheel(wheel_path)
if metadata_dict:
self.populate_metadata(metadata_dict)
def get_egg_metadata(self, metadata_dir=None, metadata_type=None):
# type: (Optional[Text], Optional[Text]) -> None
# type: (Optional[AnyStr], Optional[AnyStr]) -> None
package_indicators = [self.pyproject, self.setup_py, self.setup_cfg]
# if self.setup_py is not None and self.setup_py.exists():
metadata_dirs = []
if any([fn is not None and fn.exists() for fn in package_indicators]):
metadata_dirs = [self.extra_kwargs["build_dir"], self.egg_base, self.extra_kwargs["src_dir"]]
metadata_dirs = [
self.extra_kwargs["build_dir"],
self.egg_base,
self.extra_kwargs["src_dir"],
]
if metadata_dir is not None:
metadata_dirs = [metadata_dir] + metadata_dirs
metadata = [
get_metadata(d, pkg_name=self.name, metadata_type=metadata_type)
for d in metadata_dirs if os.path.exists(d)
for d in metadata_dirs
if os.path.exists(d)
]
metadata = next(iter(d for d in metadata if d), None)
if metadata is not None:
@@ -741,20 +830,20 @@ build-backend = "{1}"
if not self.version:
self.version = metadata.get("version", self.version)
self._requirements = frozenset(
set(self._requirements) | set([
BaseRequirement.from_req(req)
for req in metadata.get("requires", [])
])
set(self._requirements)
| set([BaseRequirement.from_req(req) for req in metadata.get("requires", [])])
)
if getattr(self.ireq, "extras", None):
for extra in self.ireq.extras:
extras = metadata.get("extras", {}).get(extra, [])
if extras:
extras_tuple = tuple([
BaseRequirement.from_req(req)
for req in ensure_reqs(tuple(extras))
if req is not None
])
extras_tuple = tuple(
[
BaseRequirement.from_req(req)
for req in ensure_reqs(tuple(extras))
if req is not None
]
)
self._extras_requirements += ((extra, extras_tuple),)
self._requirements = frozenset(
set(self._requirements) | set(extras_tuple)
@@ -776,7 +865,7 @@ build-backend = "{1}"
self.build_requires = ("setuptools", "wheel")
def get_info(self):
# type: () -> Dict[Text, Any]
# type: () -> Dict[S, Any]
if self.setup_cfg and self.setup_cfg.exists():
with cd(self.base_dir):
self.parse_setup_cfg()
@@ -800,7 +889,7 @@ build-backend = "{1}"
return self.as_dict()
def as_dict(self):
# type: () -> Dict[Text, Any]
# type: () -> Dict[S, Any]
prop_dict = {
"name": self.name,
"version": self.version,
@@ -829,8 +918,9 @@ build-backend = "{1}"
@classmethod
@lru_cache()
def from_ireq(cls, ireq, subdir=None, finder=None):
# type: (InstallRequirement, Optional[Text], Optional[PackageFinder]) -> Optional[SetupInfo]
# type: (InstallRequirement, Optional[AnyStr], Optional[PackageFinder]) -> Optional[SetupInfo]
import pip_shims.shims
if not ireq.link:
return
if ireq.link.is_wheel:
@@ -867,8 +957,7 @@ build-backend = "{1}"
download_dir = kwargs["download_dir"]
elif path is not None and os.path.isdir(path):
raise RequirementError(
"The file URL points to a directory not installable: {}"
.format(ireq.link)
"The file URL points to a directory not installable: {}".format(ireq.link)
)
ireq.build_location(kwargs["build_dir"])
src_dir = ireq.ensure_has_source_dir(kwargs["src_dir"])
@@ -884,14 +973,12 @@ build-backend = "{1}"
hashes=ireq.hashes(False),
progress_bar="off",
)
created = cls.create(
src_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs
)
created = cls.create(src_dir, subdirectory=subdir, ireq=ireq, kwargs=kwargs)
return created
@classmethod
def create(cls, base_dir, subdirectory=None, ireq=None, kwargs=None):
# type: (Text, Optional[Text], Optional[InstallRequirement], Optional[Dict[Text, Text]]) -> Optional[SetupInfo]
# type: (AnyStr, Optional[AnyStr], Optional[InstallRequirement], Optional[Dict[AnyStr, AnyStr]]) -> Optional[SetupInfo]
if not base_dir or base_dir is None:
return
+398
View File
@@ -0,0 +1,398 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import attr
import pip_shims.shims
from orderedmultidict import omdict
from six.moves.urllib.parse import quote_plus, unquote_plus
from urllib3 import util as urllib3_util
from urllib3.util import parse_url as urllib3_parse
from urllib3.util.url import Url
from ..environment import MYPY_RUNNING
if MYPY_RUNNING:
from typing import List, Tuple, Text, Union, TypeVar, Optional
from pip_shims.shims import Link
from vistir.compat import Path
_T = TypeVar("_T")
STRING_TYPE = Union[bytes, str, Text]
S = TypeVar("S", bytes, str, Text)
def _get_parsed_url(url):
# type: (S) -> Url
"""
This is a stand-in function for `urllib3.util.parse_url`
The orignal function doesn't handle special characters very well, this simply splits
out the authentication section, creates the parsed url, then puts the authentication
section back in, bypassing validation.
:return: The new, parsed URL object
:rtype: :class:`~urllib3.util.url.Url`
"""
try:
parsed = urllib3_parse(url)
except ValueError:
scheme, _, url = url.partition("://")
auth, _, url = url.rpartition("@")
url = "{scheme}://{url}".format(scheme=scheme, url=url)
parsed = urllib3_parse(url)._replace(auth=auth)
return parsed
def remove_password_from_url(url):
# type: (S) -> S
"""
Given a url, remove the password and insert 4 dashes
:param url: The url to replace the authentication in
:type url: S
:return: The new URL without authentication
:rtype: S
"""
parsed = _get_parsed_url(url)
if parsed.auth:
auth, _, _ = parsed.auth.partition(":")
return parsed._replace(auth="{auth}:----".format(auth=auth)).url
return parsed.url
@attr.s
class URI(object):
#: The target hostname, e.g. `amazon.com`
host = attr.ib(type=str)
#: The URI Scheme, e.g. `salesforce`
scheme = attr.ib(default="https", type=str)
#: The numeric port of the url if specified
port = attr.ib(default=None, type=int)
#: The url path, e.g. `/path/to/endpoint`
path = attr.ib(default="", type=str)
#: Query parameters, e.g. `?variable=value...`
query = attr.ib(default="", type=str)
#: URL Fragments, e.g. `#fragment=value`
fragment = attr.ib(default="", type=str)
#: Subdirectory fragment, e.g. `&subdirectory=blah...`
subdirectory = attr.ib(default="", type=str)
#: VCS ref this URI points at, if available
ref = attr.ib(default="", type=str)
#: The username if provided, parsed from `user:password@hostname`
username = attr.ib(default="", type=str)
#: Password parsed from `user:password@hostname`
password = attr.ib(default="", type=str, repr=False)
#: An orderedmultidict representing query fragments
query_dict = attr.ib(factory=omdict, type=omdict)
#: The name of the specified package in case it is a VCS URI with an egg fragment
name = attr.ib(default="", type=str)
#: Any extras requested from the requirement
extras = attr.ib(factory=tuple, type=tuple)
#: Whether the url was parsed as a direct pep508-style URL
is_direct_url = attr.ib(default=False, type=bool)
#: Whether the url was an implicit `git+ssh` url (passed as `git+git@`)
is_implicit_ssh = attr.ib(default=False, type=bool)
_auth = attr.ib(default=None, type=str, repr=False)
_fragment_dict = attr.ib(factory=dict, type=dict)
def _parse_query(self):
# type: () -> URI
query = self.query if self.query is not None else ""
query_dict = omdict()
queries = query.split("&")
query_items = []
for q in queries:
key, _, val = q.partition("=")
val = unquote_plus(val.replace("+", " "))
query_items.append((key, val))
query_dict.load(query_items)
return attr.evolve(self, query_dict=query_dict, query=query)
def _parse_fragment(self):
# type: () -> URI
subdirectory = self.subdirectory if self.subdirectory else ""
fragment = self.fragment if self.fragment else ""
if self.fragment is None:
return self
fragments = self.fragment.split("&")
fragment_items = {}
name = self.name if self.name else ""
extras = self.extras
for q in fragments:
key, _, val = q.partition("=")
val = unquote_plus(val.replace("+", " "))
fragment_items[key] = val
if key == "egg":
from .utils import parse_extras
name, stripped_extras = pip_shims.shims._strip_extras(val)
if stripped_extras:
extras = tuple(parse_extras(stripped_extras))
elif key == "subdirectory":
subdirectory = val
return attr.evolve(
self,
fragment_dict=fragment_items,
subdirectory=subdirectory,
fragment=fragment,
extras=extras,
name=name,
)
def _parse_auth(self):
# type: () -> URI
if self._auth:
username, _, password = self._auth.partition(":")
password = quote_plus(password)
return attr.evolve(self, username=username, password=password)
return self
def get_password(self, unquote=False, include_token=True):
# type: (bool, bool) -> str
password = self.password
if password and unquote:
password = unquote_plus(password)
else:
password = ""
return password
@staticmethod
def parse_subdirectory(url_part):
# type: (str) -> Tuple[str, Optional[str]]
subdir = None
if "&subdirectory" in url_part:
url_part, _, subdir = url_part.rpartition("&")
subdir = "&{0}".format(subdir.strip())
return url_part.strip(), subdir
@classmethod
def parse(cls, url):
# type: (S) -> URI
from .utils import DIRECT_URL_RE, split_ref_from_uri
is_direct_url = False
name_with_extras = None
is_implicit_ssh = url.strip().startswith("git+git@")
if is_implicit_ssh:
from ..utils import add_ssh_scheme_to_git_uri
url = add_ssh_scheme_to_git_uri(url)
direct_match = DIRECT_URL_RE.match(url)
if direct_match is not None:
is_direct_url = True
name_with_extras, _, url = url.partition("@")
name_with_extras = name_with_extras.strip()
url, ref = split_ref_from_uri(url.strip())
if "file:/" in url and "file:///" not in url:
url = url.replace("file:/", "file:///")
parsed = _get_parsed_url(url)
if not (parsed.scheme and parsed.host and parsed.path):
# check if this is a file uri
if not (
parsed.scheme
and parsed.path
and (parsed.scheme == "file" or parsed.scheme.endswith("+file"))
):
raise ValueError("Failed parsing URL {0!r} - Not a valid url".format(url))
parsed_dict = dict(parsed._asdict()).copy()
parsed_dict["is_direct_url"] = is_direct_url
parsed_dict["is_implicit_ssh"] = is_implicit_ssh
if name_with_extras:
fragment = ""
if parsed_dict["fragment"] is not None:
fragment = "{0}".format(parsed_dict["fragment"])
elif "&subdirectory" in parsed_dict["path"]:
path, fragment = cls.parse_subdirectory(parsed_dict["path"])
parsed_dict["path"] = path
elif ref is not None and "&subdirectory" in ref:
ref, fragment = cls.parse_subdirectory(ref)
parsed_dict["fragment"] = "egg={0}{1}".format(name_with_extras, fragment)
if ref is not None:
parsed_dict["ref"] = ref.strip()
return cls(**parsed_dict)._parse_auth()._parse_query()._parse_fragment()
def to_string(
self,
escape_password=True, # type: bool
unquote=True, # type: bool
direct=None, # type: Optional[bool]
strip_ssh=False, # type: bool
strip_ref=False, # type: bool
strip_name=False, # type: bool
strip_subdir=False, # type: bool
):
# type: (...) -> str
"""
Converts the current URI to a string, unquoting or escaping the password as needed
:param escape_password: Whether to replace password with ``----``, default True
:param escape_password: bool, optional
:param unquote: Whether to unquote url-escapes in the password, default False
:param unquote: bool, optional
:param bool direct: Whether to format as a direct URL
:param bool strip_ssh: Whether to strip the SSH scheme from the url (git only)
:param bool strip_ref: Whether to drop the VCS ref (if present)
:param bool strip_name: Whether to drop the name and extras (if present)
:param bool strip_subdir: Whether to drop the subdirectory (if present)
:return: The reconstructed string representing the URI
:rtype: str
"""
if direct is None:
direct = self.is_direct_url
if escape_password:
password = "----" if (self.password or self.username) else ""
else:
password = self.get_password(unquote=unquote)
auth = ""
if self.username:
if password:
auth = "{self.username}:{password}@".format(password=password, self=self)
else:
auth = "{self.username}@".format(self=self)
query = ""
if self.query:
query = "{query}?{self.query}".format(query=query, self=self)
if not direct:
if self.name and not strip_name:
fragment = "#egg={self.name_with_extras}".format(self=self)
elif not strip_name and (
self.extras and self.scheme and self.scheme.startswith("file")
):
from .utils import extras_to_string
fragment = extras_to_string(self.extras)
else:
fragment = ""
query = "{query}{fragment}".format(query=query, fragment=fragment)
if self.subdirectory and not strip_subdir:
query = "{query}&subdirectory={self.subdirectory}".format(
query=query, self=self
)
host_port_path = self.get_host_port_path(strip_ref=strip_ref)
url = "{self.scheme}://{auth}{host_port_path}{query}".format(
self=self, auth=auth, host_port_path=host_port_path, query=query
)
if strip_ssh:
from ..utils import strip_ssh_from_git_uri
url = strip_ssh_from_git_uri(url)
if self.name and direct and not strip_name:
return "{self.name_with_extras}@ {url}".format(self=self, url=url)
return url
def get_host_port_path(self, strip_ref=False):
# type: (bool) -> str
host = self.host if self.host else ""
if self.port:
host = "{host}:{self.port!s}".format(host=host, self=self)
path = "{self.path}".format(self=self)
if self.ref and not strip_ref:
path = "{path}@{self.ref}".format(path=path, self=self)
return "{host}{path}".format(host=host, path=path)
@property
def name_with_extras(self):
# type: () -> str
from .utils import extras_to_string
if not self.name:
return ""
extras = extras_to_string(self.extras)
return "{self.name}{extras}".format(self=self, extras=extras)
@property
def as_link(self):
# type: () -> Link
link = pip_shims.shims.Link(
self.to_string(escape_password=False, strip_ssh=False, direct=False)
)
return link
@property
def bare_url(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_name=True,
strip_ref=True,
strip_subdir=True,
)
@property
def url_without_fragment_or_ref(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_name=True,
strip_ref=True,
)
@property
def url_without_fragment(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_name=True,
)
@property
def url_without_ref(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_ref=True,
)
@property
def base_url(self):
# type: () -> str
return self.to_string(
escape_password=False, strip_ssh=self.is_implicit_ssh, direct=False
)
@property
def full_url(self):
# type: () -> str
return self.to_string(escape_password=False, strip_ssh=False, direct=False)
@property
def safe_string(self):
# type: () -> str
return self.to_string(escape_password=True, unquote=True)
@property
def unsafe_string(self):
# type: () -> str
return self.to_string(escape_password=False, unquote=True)
@property
def uri_escape(self):
# type: () -> str
return self.to_string(escape_password=False, unquote=False)
@property
def is_vcs(self):
# type: () -> bool
from ..utils import VCS_SCHEMES
return self.scheme in VCS_SCHEMES
@property
def is_file_url(self):
# type: () -> bool
return all([self.scheme, self.scheme == "file"])
def __str__(self):
# type: () -> str
return self.to_string(escape_password=True, unquote=True)
+129 -91
View File
@@ -6,14 +6,12 @@ import os
import re
import string
import sys
from collections import defaultdict
from itertools import chain, groupby
from operator import attrgetter
import six
import tomlkit
from attr import validators
from first import first
from packaging.markers import InvalidMarker, Marker, Op, Value, Variable
@@ -25,22 +23,37 @@ from vistir.compat import lru_cache
from vistir.misc import dedup
from vistir.path import is_valid_url
from ..utils import SCHEME_LIST, VCS_LIST, is_star, add_ssh_scheme_to_git_uri
from ..environment import MYPY_RUNNING
from ..utils import SCHEME_LIST, VCS_LIST, add_ssh_scheme_to_git_uri, is_star
if MYPY_RUNNING:
from typing import Union, Optional, List, Set, Any, TypeVar, Tuple, Sequence, Dict, Text
from typing import (
Union,
Optional,
List,
Set,
Any,
TypeVar,
Tuple,
Sequence,
Dict,
Text,
AnyStr,
Match,
Iterable,
)
from attr import _ValidatorType
from packaging.requirements import Requirement as PackagingRequirement
from pkg_resources import Requirement as PkgResourcesRequirement
from pkg_resources.extern.packaging.markers import (
Op as PkgResourcesOp, Variable as PkgResourcesVariable,
Value as PkgResourcesValue, Marker as PkgResourcesMarker
Op as PkgResourcesOp,
Variable as PkgResourcesVariable,
Value as PkgResourcesValue,
Marker as PkgResourcesMarker,
)
from pip_shims.shims import Link
from vistir.compat import Path
_T = TypeVar("_T")
TMarker = Union[Marker, PkgResourcesMarker]
TVariable = TypeVar("TVariable", PkgResourcesVariable, Variable)
@@ -48,6 +61,8 @@ if MYPY_RUNNING:
TOp = TypeVar("TOp", PkgResourcesOp, Op)
MarkerTuple = Tuple[TVariable, TOp, TValue]
TRequirement = Union[PackagingRequirement, PkgResourcesRequirement]
STRING_TYPE = Union[bytes, str, Text]
S = TypeVar("S", bytes, str, Text)
HASH_STRING = " --hash={0}"
@@ -63,13 +78,19 @@ NAME_RE = re.compile(NAME_WITH_EXTRAS)
SUBDIR_RE = r"(?:[&#]subdirectory=(?P<subdirectory>.*))"
URL_NAME = r"(?:#egg={0})".format(NAME_WITH_EXTRAS)
REF_RE = r"(?:@(?P<ref>{0}+)?)".format(REF)
URL = r"(?P<scheme>[^ ]+://)(?:(?P<host>[^ ]+?\.?{0}+(?P<port>:\d+)?))?(?P<pathsep>[:/])(?P<path>[^ @]+){1}?".format(ALPHA_NUMERIC, REF_RE)
PATH_RE = r"(?P<pathsep>[:/])(?P<path>[^ @]+){0}?".format(REF_RE)
PASS_RE = r"(?:(?<=:)(?P<password>[^ ]+))"
AUTH_RE = r"(?:(?P<username>[^ ]+)[:@]{0}?@)".format(PASS_RE)
HOST_RE = r"(?:{0}?(?P<host>[^ ]+?\.?{1}+(?P<port>:\d+)?))?".format(
AUTH_RE, ALPHA_NUMERIC
)
URL = r"(?P<scheme>[^ ]+://){0}{1}".format(HOST_RE, PATH_RE)
URL_RE = re.compile(r"{0}(?:{1}?{2}?)?".format(URL, URL_NAME, SUBDIR_RE))
DIRECT_URL_RE = re.compile(r"{0}\s?@\s?{1}".format(NAME_WITH_EXTRAS, URL))
def filter_none(k, v):
# type: (Text, Any) -> bool
# type: (AnyStr, Any) -> bool
if v:
return True
return False
@@ -81,16 +102,17 @@ def optional_instance_of(cls):
def create_link(link):
# type: (Text) -> Link
# type: (AnyStr) -> Link
if not isinstance(link, six.string_types):
raise TypeError("must provide a string to instantiate a new link")
from pip_shims.shims import Link
return Link(link)
def get_url_name(url):
# type: (Text) -> Text
# type: (AnyStr) -> AnyStr
"""
Given a url, derive an appropriate name to use in a pipfile.
@@ -104,11 +126,12 @@ def get_url_name(url):
def init_requirement(name):
# type: (Text) -> TRequirement
# type: (AnyStr) -> TRequirement
if not isinstance(name, six.string_types):
raise TypeError("must supply a name to generate a requirement")
from pkg_resources import Requirement
req = Requirement.parse(name)
req.vcs = None
req.local_file = None
@@ -118,7 +141,7 @@ def init_requirement(name):
def extras_to_string(extras):
# type: (Sequence) -> Text
# type: (Iterable[S]) -> S
"""Turn a list of extras into a string"""
if isinstance(extras, six.string_types):
if extras.startswith("["):
@@ -127,22 +150,23 @@ def extras_to_string(extras):
extras = [extras]
if not extras:
return ""
return "[{0}]".format(",".join(sorted(set(extras))))
return "[{0}]".format(",".join(sorted(set(extras)))) # type: ignore
def parse_extras(extras_str):
# type: (Text) -> List
# type: (AnyStr) -> List[AnyStr]
"""
Turn a string of extras into a parsed extras list
"""
from pkg_resources import Requirement
extras = Requirement.parse("fakepkg{0}".format(extras_to_string(extras_str))).extras
return sorted(dedup([extra.lower() for extra in extras]))
def specs_to_string(specs):
# type: (List[Union[Text, Specifier]]) -> Text
# type: (List[Union[STRING_TYPE, Specifier]]) -> AnyStr
"""
Turn a list of specifier tuples into a string
"""
@@ -153,20 +177,20 @@ def specs_to_string(specs):
try:
extras = ",".join(["".join(spec) for spec in specs])
except TypeError:
extras = ",".join(["".join(spec._spec) for spec in specs])
extras = ",".join(["".join(spec._spec) for spec in specs]) # type: ignore
return extras
return ""
def build_vcs_uri(
vcs, # type: Optional[Text]
uri, # type: Text
name=None, # type: Optional[Text]
ref=None, # type: Optional[Text]
subdirectory=None, # type: Optional[Text]
extras=None # type: Optional[List[Text]]
vcs, # type: Optional[S]
uri, # type: S
name=None, # type: Optional[S]
ref=None, # type: Optional[S]
subdirectory=None, # type: Optional[S]
extras=None, # type: Optional[Iterable[S]]
):
# type: (...) -> Text
# type: (...) -> STRING_TYPE
if extras is None:
extras = []
vcs_start = ""
@@ -187,46 +211,55 @@ def build_vcs_uri(
def convert_direct_url_to_url(direct_url):
# type: (Text) -> Text
# type: (AnyStr) -> AnyStr
"""
Given a direct url as defined by *PEP 508*, convert to a :class:`~pip_shims.shims.Link`
compatible URL by moving the name and extras into an **egg_fragment**.
:param str direct_url: A pep-508 compliant direct url.
:return: A reformatted URL for use with Link objects and :class:`~pip_shims.shims.InstallRequirement` objects.
:rtype: Text
:rtype: AnyStr
"""
direct_match = DIRECT_URL_RE.match(direct_url)
direct_match = DIRECT_URL_RE.match(direct_url) # type: Optional[Match]
if direct_match is None:
url_match = URL_RE.match(direct_url)
if url_match or is_valid_url(direct_url):
return direct_url
match_dict = direct_match.groupdict()
match_dict = (
{}
) # type: Dict[STRING_TYPE, Union[Tuple[STRING_TYPE, ...], STRING_TYPE]]
if direct_match is not None:
match_dict = direct_match.groupdict() # type: ignore
if not match_dict:
raise ValueError("Failed converting value to normal URL, is it a direct URL? {0!r}".format(direct_url))
raise ValueError(
"Failed converting value to normal URL, is it a direct URL? {0!r}".format(
direct_url
)
)
url_segments = [match_dict.get(s) for s in ("scheme", "host", "path", "pathsep")]
url = "".join([s for s in url_segments if s is not None])
url = "" # type: STRING_TYPE
url = "".join([s for s in url_segments if s is not None]) # type: ignore
new_url = build_vcs_uri(
None,
url,
ref=match_dict.get("ref"),
name=match_dict.get("name"),
extras=match_dict.get("extras"),
subdirectory=match_dict.get("subdirectory")
subdirectory=match_dict.get("subdirectory"),
)
return new_url
def convert_url_to_direct_url(url, name=None):
# type: (Text, Optional[Text]) -> Text
# type: (AnyStr, Optional[AnyStr]) -> AnyStr
"""
Given a :class:`~pip_shims.shims.Link` compatible URL, convert to a direct url as
defined by *PEP 508* by extracting the name and extras from the **egg_fragment**.
:param Text url: A :class:`~pip_shims.shims.InstallRequirement` compliant URL.
:param Optiona[Text] name: A name to use in case the supplied URL doesn't provide one.
:param AnyStr url: A :class:`~pip_shims.shims.InstallRequirement` compliant URL.
:param Optiona[AnyStr] name: A name to use in case the supplied URL doesn't provide one.
:return: A pep-508 compliant direct url.
:rtype: Text
:rtype: AnyStr
:raises ValueError: Raised when the URL can't be parsed or a name can't be found.
:raises TypeError: When a non-string input is provided.
@@ -266,7 +299,7 @@ def convert_url_to_direct_url(url, name=None):
def get_version(pipfile_entry):
# type: (Union[Text, Dict[Text, bool, List[Text]]]) -> Text
# type: (Union[STRING_TYPE, Dict[STRING_TYPE, Union[STRING_TYPE, bool, Iterable[STRING_TYPE]]]]) -> STRING_TYPE
if str(pipfile_entry) == "{}" or is_star(pipfile_entry):
return ""
@@ -287,7 +320,7 @@ def strip_extras_markers_from_requirement(req):
*extra == 'name'*, strip out the extras from the markers and return the cleaned
requirement
:param PackagingRequirement req: A pacakaging requirement to clean
:param PackagingRequirement req: A packaging requirement to clean
:return: A cleaned requirement
:rtype: PackagingRequirement
"""
@@ -327,8 +360,9 @@ def _strip_extras_markers(marker):
@lru_cache()
def get_setuptools_version():
# type: () -> Optional[Text]
# type: () -> Optional[STRING_TYPE]
import pkg_resources
setuptools_dist = pkg_resources.get_distribution(
pkg_resources.Requirement("setuptools")
)
@@ -336,7 +370,7 @@ def get_setuptools_version():
def get_default_pyproject_backend():
# type: () -> Text
# type: () -> STRING_TYPE
st_version = get_setuptools_version()
if st_version is not None:
parsed_st_version = parse_version(st_version)
@@ -346,19 +380,20 @@ def get_default_pyproject_backend():
def get_pyproject(path):
# type: (Union[Text, Path]) -> Tuple[List[Text], Text]
# type: (Union[STRING_TYPE, Path]) -> Optional[Tuple[List[STRING_TYPE], STRING_TYPE]]
"""
Given a base path, look for the corresponding ``pyproject.toml`` file and return its
build_requires and build_backend.
:param Text path: The root path of the project, should be a directory (will be truncated)
:param AnyStr path: The root path of the project, should be a directory (will be truncated)
:return: A 2 tuple of build requirements and the build backend
:rtype: Tuple[List[Text], Text]
:rtype: Optional[Tuple[List[AnyStr], AnyStr]]
"""
if not path:
return
from vistir.compat import Path
if not isinstance(path, Path):
path = Path(path)
if not path.is_dir():
@@ -382,19 +417,16 @@ def get_pyproject(path):
else:
requires = ["setuptools>=40.8", "wheel"]
backend = get_default_pyproject_backend()
build_system = {
"requires": requires,
"build-backend": backend
}
build_system = {"requires": requires, "build-backend": backend}
pyproject_data["build_system"] = build_system
else:
requires = build_system.get("requires", ["setuptools>=40.8", "wheel"])
backend = build_system.get("build-backend", get_default_pyproject_backend())
return (requires, backend)
return requires, backend
def split_markers_from_line(line):
# type: (Text) -> Tuple[Text, Optional[Text]]
# type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]]
"""Split markers from a dependency"""
if not any(line.startswith(uri_prefix) for uri_prefix in SCHEME_LIST):
marker_sep = ";"
@@ -408,9 +440,10 @@ def split_markers_from_line(line):
def split_vcs_method_from_uri(uri):
# type: (Text) -> Tuple[Optional[Text], Text]
# type: (AnyStr) -> Tuple[Optional[STRING_TYPE], STRING_TYPE]
"""Split a vcs+uri formatted uri into (vcs, uri)"""
vcs_start = "{0}+"
vcs = None # type: Optional[STRING_TYPE]
vcs = first([vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))])
if vcs:
vcs, uri = uri.split("+", 1)
@@ -418,14 +451,14 @@ def split_vcs_method_from_uri(uri):
def split_ref_from_uri(uri):
# type: (Text) -> Tuple[Text, Optional[Text]]
# type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]]
"""
Given a path or URI, check for a ref and split it from the path if it is present,
returning a tuple of the original input and the ref or None.
:param Text uri: The path or URI to split
:param AnyStr uri: The path or URI to split
:returns: A 2-tuple of the path or URI and the ref
:rtype: Tuple[Text, Optional[Text]]
:rtype: Tuple[AnyStr, Optional[AnyStr]]
"""
if not isinstance(uri, six.string_types):
raise TypeError("Expected a string, received {0!r}".format(uri))
@@ -474,14 +507,14 @@ def key_from_ireq(ireq):
def key_from_req(req):
"""Get an all-lowercase version of the requirement's name."""
if hasattr(req, 'key'):
if hasattr(req, "key"):
# from pkg_resources, such as installed dists for pip-sync
key = req.key
else:
# from packaging, such as install requirements from requirements.txt
key = req.name
key = key.replace('_', '-').lower()
key = key.replace("_", "-").lower()
return key
@@ -494,8 +527,8 @@ def _requirement_to_str_lowercase_name(requirement):
modified to lowercase the dependency name.
Previously, we were invoking the original Requirement.__str__ method and
lowercasing the entire result, which would lowercase the name, *and* other,
important stuff that should not be lowercased (such as the marker). See
lower-casing the entire result, which would lowercase the name, *and* other,
important stuff that should not be lower-cased (such as the marker). See
this issue for more information: https://github.com/pypa/pipenv/issues/2113.
"""
@@ -523,17 +556,17 @@ def format_requirement(ireq):
"""
if ireq.editable:
line = '-e {}'.format(ireq.link)
line = "-e {}".format(ireq.link)
else:
line = _requirement_to_str_lowercase_name(ireq.req)
if str(ireq.req.marker) != str(ireq.markers):
if not ireq.req.marker:
line = '{}; {}'.format(line, ireq.markers)
line = "{}; {}".format(line, ireq.markers)
else:
name, markers = line.split(";", 1)
markers = markers.strip()
line = '{}; ({}) and ({})'.format(name, markers, ireq.markers)
line = "{}; ({}) and ({})".format(name, markers, ireq.markers)
return line
@@ -546,7 +579,7 @@ def format_specifier(ireq):
# TODO: Ideally, this is carried over to the pip library itself
specs = ireq.specifier._specs if ireq.req is not None else []
specs = sorted(specs, key=lambda x: x._spec[1])
return ','.join(str(s) for s in specs) or '<any>'
return ",".join(str(s) for s in specs) or "<any>"
def get_pinned_version(ireq):
@@ -573,9 +606,7 @@ def get_pinned_version(ireq):
try:
specifier = ireq.specifier
except AttributeError:
raise TypeError("Expected InstallRequirement, not {}".format(
type(ireq).__name__,
))
raise TypeError("Expected InstallRequirement, not {}".format(type(ireq).__name__))
if ireq.editable:
raise ValueError("InstallRequirement is editable")
@@ -585,10 +616,8 @@ def get_pinned_version(ireq):
raise ValueError("InstallRequirement has multiple specifications")
op, version = next(iter(specifier._specs))._spec
if op not in ('==', '===') or version.endswith('.*'):
raise ValueError("InstallRequirement not pinned (is {0!r})".format(
op + version,
))
if op not in ("==", "===") or version.endswith(".*"):
raise ValueError("InstallRequirement not pinned (is {0!r})".format(op + version))
return version
@@ -624,7 +653,7 @@ def as_tuple(ireq):
"""
if not is_pinned_requirement(ireq):
raise TypeError('Expected a pinned InstallRequirement, got {}'.format(ireq))
raise TypeError("Expected a pinned InstallRequirement, got {}".format(ireq))
name = key_from_req(ireq.req)
version = first(ireq.specifier._specs)._spec[1]
@@ -686,9 +715,9 @@ def lookup_table(values, key=None, keyval=None, unique=False, use_lists=False):
if keyval is None:
if key is None:
keyval = (lambda v: v)
keyval = lambda v: v
else:
keyval = (lambda v: (key(v), v))
keyval = lambda v: (key(v), v)
if unique:
return dict(keyval(v) for v in values)
@@ -712,7 +741,7 @@ def lookup_table(values, key=None, keyval=None, unique=False, use_lists=False):
def name_from_req(req):
"""Get the name of the requirement"""
if hasattr(req, 'project_name'):
if hasattr(req, "project_name"):
# from pkg_resources, such as installed dists for pip-sync
return req.project_name
else:
@@ -742,6 +771,7 @@ def make_install_requirement(name, version, extras, markers, constraint=False):
# If no extras are specified, the extras string is blank
from pip_shims.shims import install_req_from_line
extras_string = ""
if extras:
# Sort extras for stability
@@ -749,12 +779,13 @@ def make_install_requirement(name, version, extras, markers, constraint=False):
if not markers:
return install_req_from_line(
str('{}{}=={}'.format(name, extras_string, version)),
constraint=constraint)
str("{}{}=={}".format(name, extras_string, version)), constraint=constraint
)
else:
return install_req_from_line(
str('{}{}=={}; {}'.format(name, extras_string, version, str(markers))),
constraint=constraint)
str("{}{}=={}; {}".format(name, extras_string, version, str(markers))),
constraint=constraint,
)
def version_from_ireq(ireq):
@@ -772,9 +803,10 @@ def version_from_ireq(ireq):
def clean_requires_python(candidates):
"""Get a cleaned list of all the candidates with valid specifiers in the `requires_python` attributes."""
all_candidates = []
sys_version = '.'.join(map(str, sys.version_info[:3]))
sys_version = ".".join(map(str, sys.version_info[:3]))
from packaging.version import parse as parse_version
py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', sys_version))
py_version = parse_version(os.environ.get("PIP_PYTHON_VERSION", sys_version))
for c in candidates:
from_location = attrgetter("location.requires_python")
requires_python = getattr(c, "requires_python", from_location(c))
@@ -782,7 +814,9 @@ def clean_requires_python(candidates):
# Old specifications had people setting this to single digits
# which is effectively the same as '>=digit,<digit+1'
if requires_python.isdigit():
requires_python = '>={0},<{1}'.format(requires_python, int(requires_python) + 1)
requires_python = ">={0},<{1}".format(
requires_python, int(requires_python) + 1
)
try:
specifierset = SpecifierSet(requires_python)
except InvalidSpecifier:
@@ -796,7 +830,8 @@ def clean_requires_python(candidates):
def fix_requires_python_marker(requires_python):
from packaging.requirements import Requirement as PackagingRequirement
marker_str = ''
marker_str = ""
if any(requires_python.startswith(op) for op in Specifier._operators.keys()):
spec_dict = defaultdict(set)
# We are checking first if we have leading specifier operator
@@ -804,26 +839,28 @@ def fix_requires_python_marker(requires_python):
specifierset = list(SpecifierSet(requires_python))
# for multiple specifiers, the correct way to represent that in
# a specifierset is `Requirement('fakepkg; python_version<"3.0,>=2.6"')`
marker_key = Variable('python_version')
marker_key = Variable("python_version")
for spec in specifierset:
operator, val = spec._spec
cleaned_val = Value(val).serialize().replace('"', "")
spec_dict[Op(operator).serialize()].add(cleaned_val)
marker_str = ' and '.join([
"{0}{1}'{2}'".format(marker_key.serialize(), op, ','.join(vals))
for op, vals in spec_dict.items()
])
marker_to_add = PackagingRequirement('fakepkg; {0}'.format(marker_str)).marker
marker_str = " and ".join(
[
"{0}{1}'{2}'".format(marker_key.serialize(), op, ",".join(vals))
for op, vals in spec_dict.items()
]
)
marker_to_add = PackagingRequirement("fakepkg; {0}".format(marker_str)).marker
return marker_to_add
def normalize_name(pkg):
# type: (Text) -> Text
# type: (AnyStr) -> AnyStr
"""Given a package name, return its normalized, non-canonicalized form.
:param Text pkg: The name of a package
:param AnyStr pkg: The name of a package
:return: A normalized package name
:rtype: Text
:rtype: AnyStr
"""
assert isinstance(pkg, six.string_types)
@@ -831,12 +868,12 @@ def normalize_name(pkg):
def get_name_variants(pkg):
# type: (Text) -> Set[Text]
# type: (STRING_TYPE) -> Set[STRING_TYPE]
"""
Given a packager name, get the variants of its name for both the canonicalized
and "safe" forms.
:param Text pkg: The package to lookup
:param AnyStr pkg: The package to lookup
:returns: A list of names.
:rtype: Set
"""
@@ -845,6 +882,7 @@ def get_name_variants(pkg):
raise TypeError("must provide a string to derive package names")
from pkg_resources import safe_name
from packaging.utils import canonicalize_name
pkg = pkg.lower()
names = {safe_name(pkg), canonicalize_name(pkg), pkg.replace("-", "_")}
return names
+72 -38
View File
@@ -4,27 +4,53 @@ from __future__ import absolute_import, print_function
import contextlib
import logging
import os
import six
import sys
import tomlkit
import vistir
six.add_move(six.MovedAttribute("Mapping", "collections", "collections.abc")) # type: ignore # noqa
six.add_move(six.MovedAttribute("Sequence", "collections", "collections.abc")) # type: ignore # noqa
six.add_move(six.MovedAttribute("Set", "collections", "collections.abc")) # type: ignore # noqa
six.add_move(six.MovedAttribute("ItemsView", "collections", "collections.abc")) # type: ignore # noqa
from six.moves import Mapping, Sequence, Set, ItemsView # type: ignore # noqa
from six.moves.urllib.parse import urlparse, urlsplit, urlunparse
import pip_shims.shims
import six
import tomlkit
import vistir
from six.moves.urllib.parse import urlparse, urlsplit, urlunparse
from vistir.compat import Path
from vistir.path import is_valid_url, ensure_mkdir_p, create_tracked_tempdir
from vistir.path import create_tracked_tempdir, ensure_mkdir_p, is_valid_url
from .environment import MYPY_RUNNING
# fmt: off
six.add_move(
six.MovedAttribute("Mapping", "collections", "collections.abc")
) # type: ignore # noqa # isort:skip
six.add_move(
six.MovedAttribute("Sequence", "collections", "collections.abc")
) # type: ignore # noqa # isort:skip
six.add_move(
six.MovedAttribute("Set", "collections", "collections.abc")
) # type: ignore # noqa # isort:skip
six.add_move(
six.MovedAttribute("ItemsView", "collections", "collections.abc")
) # type: ignore # noqa
from six.moves import ItemsView, Mapping, Sequence, Set # type: ignore # noqa # isort:skip
# fmt: on
if MYPY_RUNNING:
from typing import Dict, Any, Optional, Union, Tuple, List, Iterable, Generator, Text
from typing import (
Dict,
Any,
Optional,
Union,
Tuple,
List,
Iterable,
Generator,
Text,
TypeVar,
)
STRING_TYPE = Union[bytes, str, Text]
S = TypeVar("S", bytes, str, Text)
PipfileEntryType = Union[STRING_TYPE, bool, Tuple[STRING_TYPE], List[STRING_TYPE]]
PipfileType = Union[STRING_TYPE, Dict[STRING_TYPE, PipfileEntryType]]
VCS_LIST = ("git", "svn", "hg", "bzr")
@@ -74,7 +100,7 @@ VCS_SCHEMES = [
def is_installable_dir(path):
# type: (Text) -> bool
# type: (STRING_TYPE) -> bool
if pip_shims.shims.is_installable_dir(path):
return True
pyproject_path = os.path.join(path, "pyproject.toml")
@@ -88,7 +114,7 @@ def is_installable_dir(path):
def strip_ssh_from_git_uri(uri):
# type: (Text) -> Text
# type: (S) -> S
"""Return git+ssh:// formatted URI to git+git@ format"""
if isinstance(uri, six.string_types):
if "git+ssh://" in uri:
@@ -105,8 +131,8 @@ def strip_ssh_from_git_uri(uri):
def add_ssh_scheme_to_git_uri(uri):
# type: (Text) -> Text
"""Cleans VCS uris from pip format"""
# type: (S) -> S
"""Cleans VCS uris from pipenv.patched.notpip format"""
if isinstance(uri, six.string_types):
# Add scheme for parsing purposes, this is also what pip does
if uri.startswith("git+") and "://" not in uri:
@@ -120,7 +146,7 @@ def add_ssh_scheme_to_git_uri(uri):
def is_vcs(pipfile_entry):
# type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool
# type: (PipfileType) -> bool
"""Determine if dictionary entry from Pipfile is for a vcs dependency."""
if isinstance(pipfile_entry, Mapping):
return any(key for key in pipfile_entry.keys() if key in VCS_LIST)
@@ -135,7 +161,7 @@ def is_vcs(pipfile_entry):
def is_editable(pipfile_entry):
# type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool
# type: (PipfileType) -> bool
if isinstance(pipfile_entry, Mapping):
return pipfile_entry.get("editable", False) is True
if isinstance(pipfile_entry, six.string_types):
@@ -144,7 +170,7 @@ def is_editable(pipfile_entry):
def multi_split(s, split):
# type: (Text, Iterable[Text]) -> List[Text]
# type: (S, Iterable[S]) -> List[S]
"""Splits on multiple given separators."""
for r in split:
s = s.replace(r, "|")
@@ -152,14 +178,14 @@ def multi_split(s, split):
def is_star(val):
# type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool
# type: (PipfileType) -> bool
return (isinstance(val, six.string_types) and val == "*") or (
isinstance(val, Mapping) and val.get("version", "") == "*"
)
def convert_entry_to_path(path):
# type: (Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]) -> Text
# type: (Dict[S, Union[S, bool, Tuple[S], List[S]]]) -> S
"""Convert a pipfile entry to a string"""
if not isinstance(path, Mapping):
@@ -177,7 +203,7 @@ def convert_entry_to_path(path):
def is_installable_file(path):
# type: (Union[Text, Dict[Text, Union[Text, bool, Tuple[Text], List[Text]]]]) -> bool
# type: (PipfileType) -> bool
"""Determine if a path can potentially be installed"""
from packaging import specifiers
@@ -196,7 +222,11 @@ def is_installable_file(path):
return False
parsed = urlparse(path)
is_local = (not parsed.scheme or parsed.scheme == "file" or (len(parsed.scheme) == 1 and os.name == "nt"))
is_local = (
not parsed.scheme
or parsed.scheme == "file"
or (len(parsed.scheme) == 1 and os.name == "nt")
)
if parsed.scheme and parsed.scheme == "file":
path = vistir.compat.fs_decode(vistir.path.url_to_path(path))
normalized_path = vistir.path.normalize_path(path)
@@ -204,7 +234,9 @@ def is_installable_file(path):
return False
is_archive = pip_shims.shims.is_archive_file(normalized_path)
is_local_project = os.path.isdir(normalized_path) and is_installable_dir(normalized_path)
is_local_project = os.path.isdir(normalized_path) and is_installable_dir(
normalized_path
)
if is_local and is_local_project or is_archive:
return True
@@ -217,11 +249,13 @@ def is_installable_file(path):
def get_dist_metadata(dist):
import pkg_resources
from email.parser import FeedParser
if (isinstance(dist, pkg_resources.DistInfoDistribution) and
dist.has_metadata('METADATA')):
metadata = dist.get_metadata('METADATA')
elif dist.has_metadata('PKG-INFO'):
metadata = dist.get_metadata('PKG-INFO')
if isinstance(dist, pkg_resources.DistInfoDistribution) and dist.has_metadata(
"METADATA"
):
metadata = dist.get_metadata("METADATA")
elif dist.has_metadata("PKG-INFO"):
metadata = dist.get_metadata("PKG-INFO")
else:
metadata = ""
@@ -231,7 +265,7 @@ def get_dist_metadata(dist):
def get_setup_paths(base_path, subdirectory=None):
# type: (Text, Optional[Text]) -> Dict[Text, Optional[Text]]
# type: (S, Optional[S]) -> Dict[S, Optional[S]]
if base_path is None:
raise TypeError("must provide a path to derive setup paths from")
setup_py = os.path.join(base_path, "setup.py")
@@ -251,12 +285,12 @@ def get_setup_paths(base_path, subdirectory=None):
return {
"setup_py": setup_py if os.path.exists(setup_py) else None,
"setup_cfg": setup_cfg if os.path.exists(setup_cfg) else None,
"pyproject_toml": pyproject_toml if os.path.exists(pyproject_toml) else None
"pyproject_toml": pyproject_toml if os.path.exists(pyproject_toml) else None,
}
def prepare_pip_source_args(sources, pip_args=None):
# type: (List[Dict[Text, Union[Text, bool]]], Optional[List[Text]]) -> List[Text]
# type: (List[Dict[S, Union[S, bool]]], Optional[List[S]]) -> List[S]
if pip_args is None:
pip_args = []
if sources:
@@ -264,7 +298,9 @@ def prepare_pip_source_args(sources, pip_args=None):
pip_args.extend(["-i", sources[0]["url"]]) # type: ignore
# Trust the host if it's not verified.
if not sources[0].get("verify_ssl", True):
pip_args.extend(["--trusted-host", urlparse(sources[0]["url"]).hostname]) # type: ignore
pip_args.extend(
["--trusted-host", urlparse(sources[0]["url"]).hostname]
) # type: ignore
# Add additional sources as extra indexes.
if len(sources) > 1:
for source in sources[1:]:
@@ -284,7 +320,7 @@ def _ensure_dir(path):
@contextlib.contextmanager
def ensure_setup_py(base):
# type: (Text) -> Generator[None, None, None]
# type: (STRING_TYPE) -> Generator[None, None, None]
if not base:
base = create_tracked_tempdir(prefix="requirementslib-setup")
base_dir = Path(base)
@@ -413,9 +449,7 @@ def get_path(root, path, default=_UNSET):
cur = cur[seg]
except (ValueError, KeyError, IndexError, TypeError):
if not getattr(cur, "__iter__", None):
exc = TypeError(
"%r object is not indexable" % type(cur).__name__
)
exc = TypeError("%r object is not indexable" % type(cur).__name__)
raise PathAccessError(exc, seg, path)
except PathAccessError:
if default is _UNSET:
+10 -10
View File
@@ -13,22 +13,22 @@ python-dotenv==0.10.1
first==2.0.1
iso8601==0.1.12
jinja2==2.10
markupsafe==1.0
parse==1.9.0
markupsafe==1.1.1
parse==1.11.1
pathlib2==2.3.3
scandir==1.9
pipdeptree==0.13.1
pipdeptree==0.13.2
pipreqs==0.4.9
docopt==0.6.2
yarg==0.1.9
pythonfinder==1.1.10
pythonfinder==1.2.0
requests==2.21.0
chardet==3.0.4
idna==2.8
urllib3==1.24.1
certifi==2018.11.29
requirementslib==1.4.0
attrs==18.2.0
requirementslib==1.4.2
attrs==19.1.0
distlib==0.2.8
packaging==19.0
pyparsing==2.3.1
@@ -40,14 +40,14 @@ semver==2.8.1
shutilwhich==1.1.0
toml==0.10.0
cached-property==1.5.1
vistir==0.3.0
vistir==0.3.1
pip-shims==0.3.2
enum34==1.1.6
yaspin==0.14.0
yaspin==0.14.1
cerberus==1.2
git+https://github.com/sarugaku/passa.git@master#egg=passa
cursor==1.2.0
resolvelib==0.2.2
backports.functools_lru_cache==1.5
pep517==0.5.0
pytoml==0.1.20
git+https://github.com/sarugaku/passa.git@master#egg=passa
orderedmultidict==1.0
+16 -14
View File
@@ -3,39 +3,39 @@ from __future__ import absolute_import, unicode_literals
from .compat import (
NamedTemporaryFile,
StringIO,
TemporaryDirectory,
partialmethod,
to_native_string,
StringIO,
)
from .contextmanagers import (
atomic_open_for_write,
cd,
open_file,
replaced_stream,
spinner,
temp_environ,
temp_path,
spinner,
replaced_stream
)
from .cursor import hide_cursor, show_cursor
from .misc import (
StreamWrapper,
chunked,
decode_for_output,
divide,
get_wrapped_stream,
load_path,
partialclass,
run,
shell_escape,
decode_for_output,
to_text,
to_bytes,
take,
chunked,
divide,
get_wrapped_stream,
StreamWrapper
to_bytes,
to_text,
)
from .path import mkdir_p, rmtree, create_tracked_tempdir, create_tracked_tempfile
from .path import create_tracked_tempdir, create_tracked_tempfile, mkdir_p, rmtree
from .spin import create_spinner
__version__ = '0.3.0'
__version__ = "0.3.1"
__all__ = [
@@ -67,5 +67,7 @@ __all__ = [
"StringIO",
"get_wrapped_stream",
"StreamWrapper",
"replaced_stream"
"replaced_stream",
"show_cursor",
"hide_cursor",
]
+1 -5
View File
@@ -4,8 +4,4 @@ from __future__ import absolute_import, unicode_literals
from .functools import partialmethod
from .tempfile import NamedTemporaryFile
__all__ = [
"NamedTemporaryFile",
"partialmethod"
]
__all__ = ["NamedTemporaryFile", "partialmethod"]
+12 -12
View File
@@ -3,8 +3,7 @@ from __future__ import absolute_import, unicode_literals
from functools import partial
__all__ = ["partialmethod",]
__all__ = ["partialmethod"]
class partialmethod(object):
@@ -16,8 +15,7 @@ class partialmethod(object):
def __init__(self, func, *args, **keywords):
if not callable(func) and not hasattr(func, "__get__"):
raise TypeError("{!r} is not callable or a descriptor"
.format(func))
raise TypeError("{!r} is not callable or a descriptor".format(func))
# func could be a descriptor like classmethod which isn't callable,
# so we can't inherit from partial (it verifies func is callable)
@@ -36,26 +34,28 @@ class partialmethod(object):
def __repr__(self):
args = ", ".join(map(repr, self.args))
keywords = ", ".join("{}={!r}".format(k, v)
for k, v in self.keywords.items())
keywords = ", ".join("{}={!r}".format(k, v) for k, v in self.keywords.items())
format_string = "{module}.{cls}({func}, {args}, {keywords})"
return format_string.format(module=self.__class__.__module__,
cls=self.__class__.__qualname__,
func=self.func,
args=args,
keywords=keywords)
return format_string.format(
module=self.__class__.__module__,
cls=self.__class__.__qualname__,
func=self.func,
args=args,
keywords=keywords,
)
def _make_unbound_method(self):
def _method(*args, **keywords):
call_keywords = self.keywords.copy()
call_keywords.update(keywords)
if len(args) > 1:
cls_or_self, rest = args[0], tuple(args[1:],)
cls_or_self, rest = args[0], tuple(args[1:])
else:
cls_or_self = args[0]
rest = tuple()
call_args = (cls_or_self,) + self.args + tuple(rest)
return self.func(*call_args, **call_keywords)
_method.__isabstractmethod__ = self.__isabstractmethod__
_method._partialmethod = self
return _method
+5 -8
View File
@@ -5,7 +5,6 @@ import functools
import io
import os
import sys
from tempfile import _bin_openflags, _mkstemp_inner, gettempdir
import six
@@ -175,7 +174,7 @@ def NamedTemporaryFile(
prefix=None,
dir=None,
delete=True,
wrapper_class_override=None
wrapper_class_override=None,
):
"""Create and return a temporary file.
Arguments:
@@ -203,13 +202,11 @@ def NamedTemporaryFile(
else:
(fd, name) = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
try:
file = io.open(
fd, mode, buffering=buffering, newline=newline, encoding=encoding
)
file = io.open(fd, mode, buffering=buffering, newline=newline, encoding=encoding)
if wrapper_class_override is not None:
return type(
str("_TempFileWrapper"), (wrapper_class_override, object), {}
)(file, name, delete)
return type(str("_TempFileWrapper"), (wrapper_class_override, object), {})(
file, name, delete
)
else:
return _TemporaryFileWrapper(file, name, delete)
+11 -4
View File
@@ -1,12 +1,12 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, unicode_literals
import itertools
import re
import shlex
import six
__all__ = ["ScriptEmptyError", "Script"]
@@ -14,6 +14,12 @@ class ScriptEmptyError(ValueError):
pass
def _quote_if_contains(value, pattern):
if next(re.finditer(pattern, value), None):
return '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', value))
return value
class Script(object):
"""Parse a script line (in Pipfile's [scripts] section).
@@ -72,7 +78,8 @@ class Script(object):
See also: https://docs.python.org/3/library/subprocess.html#converting-argument-sequence
"""
return " ".join(
arg if not next(re.finditer(r'\s', arg), None)
else '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', arg))
for arg in self._parts
itertools.chain(
[_quote_if_contains(self.command, r"[\s^()]")],
(_quote_if_contains(arg, r"[\s^]") for arg in self.args),
)
)
+30 -11
View File
@@ -6,11 +6,11 @@ import errno
import os
import sys
import warnings
from tempfile import mkdtemp
import six
from .backports.tempfile import NamedTemporaryFile as _NamedTemporaryFile
__all__ = [
"Path",
@@ -35,19 +35,20 @@ __all__ = [
"fs_encode",
"fs_decode",
"_fs_encode_errors",
"_fs_decode_errors"
"_fs_decode_errors",
]
if sys.version_info >= (3, 5):
from pathlib import Path
from functools import lru_cache
else:
from pathlib2 import Path
from pipenv.vendor.pathlib2 import Path
from pipenv.vendor.backports.functools_lru_cache import lru_cache
from .backports.tempfile import NamedTemporaryFile as _NamedTemporaryFile
if sys.version_info < (3, 3):
from pipenv.vendor.backports.shutil_get_terminal_size import get_terminal_size
NamedTemporaryFile = _NamedTemporaryFile
else:
from tempfile import NamedTemporaryFile
@@ -89,6 +90,7 @@ if six.PY2:
class IsADirectoryError(OSError):
"""The command does not work on directories"""
pass
class FileExistsError(OSError):
@@ -96,19 +98,35 @@ if six.PY2:
self.errno = errno.EEXIST
super(FileExistsError, self).__init__(*args, **kwargs)
else:
from builtins import (
ResourceWarning, FileNotFoundError, PermissionError, IsADirectoryError,
FileExistsError
ResourceWarning,
FileNotFoundError,
PermissionError,
IsADirectoryError,
FileExistsError,
)
from io import StringIO
six.add_move(six.MovedAttribute("Iterable", "collections", "collections.abc")) # type: ignore
six.add_move(six.MovedAttribute("Mapping", "collections", "collections.abc")) # type: ignore
six.add_move(six.MovedAttribute("Sequence", "collections", "collections.abc")) # type: ignore
six.add_move(
six.MovedAttribute("Iterable", "collections", "collections.abc")
) # type: ignore
six.add_move(
six.MovedAttribute("Mapping", "collections", "collections.abc")
) # type: ignore
six.add_move(
six.MovedAttribute("Sequence", "collections", "collections.abc")
) # type: ignore
six.add_move(six.MovedAttribute("Set", "collections", "collections.abc")) # type: ignore
six.add_move(six.MovedAttribute("ItemsView", "collections", "collections.abc")) # type: ignore
from six.moves import Iterable, Mapping, Sequence, Set, ItemsView # type: ignore # noqa
six.add_move(
six.MovedAttribute("ItemsView", "collections", "collections.abc")
) # type: ignore
# fmt: off
from six.moves import ItemsView, Iterable, Mapping, Sequence, Set # type: ignore # noqa # isort:skip
# fmt: on
if not sys.warnoptions:
warnings.simplefilter("default", ResourceWarning)
@@ -282,6 +300,7 @@ else:
def to_native_string(string):
from .misc import to_text, to_bytes
if six.PY2:
return to_bytes(string)
return to_text(string)
+20 -9
View File
@@ -1,11 +1,10 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, unicode_literals, print_function
from __future__ import absolute_import, print_function, unicode_literals
import io
import os
import stat
import sys
from contextlib import contextmanager
import six
@@ -13,10 +12,15 @@ import six
from .compat import NamedTemporaryFile, Path
from .path import is_file_url, is_valid_url, path_to_url, url_to_path
__all__ = [
"temp_environ", "temp_path", "cd", "atomic_open_for_write", "open_file", "spinner",
"dummy_spinner", "replaced_stream"
"temp_environ",
"temp_path",
"cd",
"atomic_open_for_write",
"open_file",
"spinner",
"dummy_spinner",
"replaced_stream",
]
@@ -104,7 +108,13 @@ def dummy_spinner(spin_type, text, **kwargs):
@contextmanager
def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False, write_to_stdout=True):
def spinner(
spinner_name=None,
start_text=None,
handler_map=None,
nospin=False,
write_to_stdout=True,
):
"""Get a spinner object or a dummy spinner to wrap a context.
:param str spinner_name: A spinner type e.g. "dots" or "bouncingBar" (default: {"bouncingBar"})
@@ -120,6 +130,7 @@ def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False,
"""
from .spin import create_spinner
has_yaspin = None
try:
import yaspin
@@ -146,7 +157,7 @@ def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False,
handler_map=handler_map,
nospin=nospin,
use_yaspin=use_yaspin,
write_to_stdout=write_to_stdout
write_to_stdout=write_to_stdout,
) as _spinner:
yield _spinner
@@ -267,8 +278,8 @@ def open_file(link, session=None, stream=True):
if os.path.isdir(local_path):
raise ValueError("Cannot open directory for read: {}".format(link))
else:
with io.open(local_path, "rb") as local_file:
yield local_file
with io.open(local_path, "rb") as local_file:
yield local_file
else:
# Remote URL
headers = {"Accept-Encoding": "identity"}
+77
View File
@@ -0,0 +1,77 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import ctypes
import os
import sys
__all__ = ["hide_cursor", "show_cursor"]
class CONSOLE_CURSOR_INFO(ctypes.Structure):
_fields_ = [("dwSize", ctypes.c_int), ("bVisible", ctypes.c_int)]
WIN_STDERR_HANDLE_ID = ctypes.c_ulong(-12)
WIN_STDOUT_HANDLE_ID = ctypes.c_ulong(-11)
def get_stream_handle(stream=sys.stdout):
"""
Get the OS appropriate handle for the corresponding output stream.
:param str stream: The the stream to get the handle for
:return: A handle to the appropriate stream, either a ctypes buffer
or **sys.stdout** or **sys.stderr**.
"""
handle = stream
if os.name == "nt":
from ctypes import windll
handle_id = WIN_STDOUT_HANDLE_ID
handle = windll.kernel32.GetStdHandle(handle_id)
return handle
def hide_cursor(stream=sys.stdout):
"""
Hide the console cursor on the given stream
:param stream: The name of the stream to get the handle for
:return: None
:rtype: None
"""
handle = get_stream_handle(stream=stream)
if os.name == "nt":
from ctypes import windll
cursor_info = CONSOLE_CURSOR_INFO()
windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(cursor_info))
cursor_info.visible = False
windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(cursor_info))
else:
handle.write("\033[?25l")
handle.flush()
def show_cursor(stream=sys.stdout):
"""
Show the console cursor on the given stream
:param stream: The name of the stream to get the handle for
:return: None
:rtype: None
"""
handle = get_stream_handle(stream=stream)
if os.name == "nt":
from ctypes import windll
cursor_info = CONSOLE_CURSOR_INFO()
windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(cursor_info))
cursor_info.visible = True
windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(cursor_info))
else:
handle.write("\033[?25h")
handle.flush()
+94 -74
View File
@@ -1,14 +1,13 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, unicode_literals, print_function
from __future__ import absolute_import, print_function, unicode_literals
import io
import json
import logging
import locale
import logging
import os
import subprocess
import sys
from collections import OrderedDict
from functools import partial
from itertools import islice, tee
@@ -16,10 +15,11 @@ from itertools import islice, tee
import six
from .cmdparse import Script
from .compat import Path, fs_str, partialmethod, to_native_string, Iterable, StringIO
from .compat import Iterable, Path, StringIO, fs_str, partialmethod, to_native_string
from .contextmanagers import spinner as spinner
if os.name != "nt":
class WindowsError(OSError):
pass
@@ -144,6 +144,55 @@ def _spawn_subprocess(script, env=None, block=True, cwd=None, combine_stderr=Tru
return subprocess.Popen(script.cmdify(), **options)
def _read_streams(stream_dict):
results = {}
for outstream in stream_dict.keys():
stream = stream_dict[outstream]
if not stream:
results[outstream] = None
continue
line = to_text(stream.readline())
if not line:
results[outstream] = None
continue
line = to_text("{0}".format(line.rstrip()))
results[outstream] = line
return results
def get_stream_results(cmd_instance, verbose, maxlen, spinner=None, stdout_allowed=False):
stream_results = {"stdout": [], "stderr": []}
streams = {"stderr": cmd_instance.stderr, "stdout": cmd_instance.stdout}
while True:
stream_contents = _read_streams(streams)
stdout_line = stream_contents["stdout"]
stderr_line = stream_contents["stderr"]
if not (stdout_line or stderr_line):
break
for stream_name in stream_contents.keys():
if stream_contents[stream_name] and stream_name in stream_results:
line = stream_contents[stream_name]
stream_results[stream_name].append(line)
display_line = fs_str("{0}".format(line))
if len(display_line) > maxlen:
display_line = "{0}...".format(display_line[:maxlen])
if verbose:
use_stderr = not stdout_allowed or stream_name != "stdout"
if spinner:
target = spinner.stderr if use_stderr else spinner.stdout
spinner.hide_and_write(display_line, target=target)
else:
target = sys.stderr if use_stderr else sys.stdout
target.write(display_line)
target.flush()
if spinner:
spinner.text = to_native_string(
"{0} {1}".format(spinner.text, display_line)
)
continue
return stream_results
def _create_subprocess(
cmd,
env=None,
@@ -155,74 +204,35 @@ def _create_subprocess(
combine_stderr=False,
display_limit=200,
start_text="",
write_to_stdout=True
write_to_stdout=True,
):
if not env:
env = os.environ.copy()
try:
c = _spawn_subprocess(cmd, env=env, block=block, cwd=cwd,
combine_stderr=combine_stderr)
except Exception as exc:
c = _spawn_subprocess(
cmd, env=env, block=block, cwd=cwd, combine_stderr=combine_stderr
)
except Exception:
import traceback
formatted_tb = "".join(traceback.format_exception(*sys.exc_info()))
sys.stderr.write("Error while executing command %s:" % " ".join(cmd._parts))
sys.stderr.write(formatted_tb)
raise
if not block:
c.stdin.close()
output = []
err = []
spinner_orig_text = None
if spinner:
spinner_orig_text = getattr(spinner, "text", None)
if spinner_orig_text is None:
spinner_orig_text = start_text if start_text is not None else ""
streams = {
"stdout": c.stdout,
"stderr": c.stderr
}
while True:
stdout_line = None
stderr_line = None
for outstream in streams.keys():
stream = streams[outstream]
if not stream:
continue
line = to_text(stream.readline())
if not line:
continue
line = to_text("{0}".format(line.rstrip()))
if outstream == "stderr":
stderr_line = line
else:
stdout_line = line
if not (stdout_line or stderr_line):
break
if stderr_line is not None:
err.append(stderr_line)
err_line = fs_str("{0}".format(stderr_line))
if verbose and err_line is not None:
if spinner:
spinner.hide_and_write(err_line, target=spinner.stderr)
else:
sys.stderr.write(err_line)
sys.stderr.flush()
if stdout_line is not None:
output.append(stdout_line)
display_line = fs_str("{0}".format(stdout_line))
if len(stdout_line) > display_limit:
display_line = "{0}...".format(stdout_line[:display_limit])
if verbose and display_line is not None:
if spinner:
target = spinner.stdout if write_to_stdout else spinner.stderr
spinner.hide_and_write(display_line, target=target)
else:
target = sys.stdout if write_to_stdout else sys.stderr
target.write(display_line)
target.flush()
if spinner:
spinner.text = to_native_string("{0} {1}".format(spinner_orig_text, display_line))
continue
spinner_orig_text = ""
if spinner and getattr(spinner, "text", None) is not None:
spinner_orig_text = spinner.text
if not spinner_orig_text and start_text is not None:
spinner_orig_text = start_text
stream_results = get_stream_results(
c,
verbose=verbose,
maxlen=display_limit,
spinner=spinner,
stdout_allowed=write_to_stdout,
)
try:
c.wait()
finally:
@@ -237,6 +247,8 @@ def _create_subprocess(
spinner.ok(to_native_string("✔ Complete"))
else:
spinner.ok(to_native_string("Complete"))
output = stream_results["stdout"]
err = stream_results["stderr"]
c.out = "\n".join(output) if output else ""
c.err = "\n".join(err) if err else ""
else:
@@ -261,7 +273,7 @@ def run(
spinner_name=None,
combine_stderr=True,
display_limit=200,
write_to_stdout=True
write_to_stdout=True,
):
"""Use `subprocess.Popen` to get the output of a command and decode it.
@@ -303,8 +315,12 @@ def run(
if block or not return_object:
combine_stderr = False
start_text = ""
with spinner(spinner_name=spinner_name, start_text=start_text, nospin=nospin,
write_to_stdout=write_to_stdout) as sp:
with spinner(
spinner_name=spinner_name,
start_text=start_text,
nospin=nospin,
write_to_stdout=write_to_stdout,
) as sp:
return _create_subprocess(
cmd,
env=_env,
@@ -315,7 +331,7 @@ def run(
spinner=sp,
combine_stderr=combine_stderr,
start_text=start_text,
write_to_stdout=True
write_to_stdout=True,
)
@@ -331,8 +347,9 @@ def load_path(python):
"""
python = Path(python).as_posix()
out, err = run([python, "-c", "import json, sys; print(json.dumps(sys.path))"],
nospin=True)
out, err = run(
[python, "-c", "import json, sys; print(json.dumps(sys.path))"], nospin=True
)
if out:
return json.loads(out)
else:
@@ -445,7 +462,7 @@ def to_text(string, encoding="utf-8", errors=None):
string = six.text_type(bytes(string), encoding, errors)
else:
string = string.decode(encoding, errors)
except UnicodeDecodeError as e:
except UnicodeDecodeError:
string = " ".join(to_text(arg, encoding, errors) for arg in string)
return string
@@ -528,8 +545,8 @@ def get_output_encoding(source_encoding):
"""
if source_encoding is not None:
if get_canonical_encoding_name(source_encoding) == 'ascii':
return 'utf-8'
if get_canonical_encoding_name(source_encoding) == "ascii":
return "utf-8"
return get_canonical_encoding_name(source_encoding)
return get_canonical_encoding_name(PREFERRED_ENCODING)
@@ -542,7 +559,7 @@ def _encode(output, encoding=None, errors=None, translation_map=None):
except (UnicodeDecodeError, UnicodeEncodeError):
if translation_map is not None:
if six.PY2:
output = unicode.translate(
output = unicode.translate( # noqa: F821
to_text(output, encoding=encoding, errors=errors), translation_map
)
else:
@@ -573,8 +590,9 @@ def decode_for_output(output, target_stream=None, translation_map=None):
try:
output = _encode(output, encoding=encoding, translation_map=translation_map)
except (UnicodeDecodeError, UnicodeEncodeError):
output = _encode(output, encoding=encoding, errors="replace",
translation_map=translation_map)
output = _encode(
output, encoding=encoding, errors="replace", translation_map=translation_map
)
return to_text(output, encoding=encoding, errors="replace")
@@ -589,6 +607,7 @@ def get_canonical_encoding_name(name):
"""
import codecs
try:
codec = codecs.lookup(name)
except LookupError:
@@ -629,8 +648,9 @@ class StreamWrapper(io.TextIOWrapper):
# borrowed from click's implementation of stream wrappers, see
# https://github.com/pallets/click/blob/6cafd32/click/_compat.py#L64
if six.PY2:
def write(self, x):
if isinstance(x, (str, buffer, bytearray)):
if isinstance(x, (str, buffer, bytearray)): # noqa: F821
try:
self.flush()
except Exception:
+20 -22
View File
@@ -1,5 +1,5 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, unicode_literals, print_function
from __future__ import absolute_import, print_function, unicode_literals
import atexit
import errno
@@ -11,23 +11,21 @@ import stat
import warnings
import six
from six.moves import urllib_parse
from six.moves.urllib import request as urllib_request
from .backports.tempfile import _TemporaryFileWrapper
from .compat import (
_NamedTemporaryFile,
Path,
ResourceWarning,
TemporaryDirectory,
_fs_encoding,
_NamedTemporaryFile,
finalize,
fs_decode,
fs_encode
fs_encode,
)
__all__ = [
"check_for_unc_path",
"get_converted_relative_path",
@@ -51,7 +49,11 @@ __all__ = [
if os.name == "nt":
warnings.filterwarnings("ignore", category=DeprecationWarning, message="The Windows bytes API has been deprecated.*")
warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
message="The Windows bytes API has been deprecated.*",
)
def unicode_path(path):
@@ -93,9 +95,11 @@ def normalize_path(path):
:rtype: str
"""
return os.path.normpath(os.path.normcase(
os.path.abspath(os.path.expandvars(os.path.expanduser(str(path))))
))
return os.path.normpath(
os.path.normcase(
os.path.abspath(os.path.expandvars(os.path.expanduser(str(path))))
)
)
def is_in_path(path, parent):
@@ -233,7 +237,7 @@ def mkdir_p(newdir, mode=0o777):
target = os.path.join(head, tail)
if os.path.exists(target) and os.path.isfile(target):
raise OSError(
"A file with the same name as the desired dir, '{0}', already exists.".format(
"A file with the same name as the desired dir, '{0}', already exists.".format(
fs_decode(newdir)
)
)
@@ -307,7 +311,7 @@ def set_write_bit(fn):
except AttributeError:
pass
for root, dirs, files in os.walk(fn, topdown=False):
for dir_ in [os.path.join(root,d) for d in dirs]:
for dir_ in [os.path.join(root, d) for d in dirs]:
set_write_bit(dir_)
for file_ in [os.path.join(root, f) for f in files]:
set_write_bit(file_)
@@ -332,9 +336,7 @@ def rmtree(directory, ignore_errors=False, onerror=None):
if onerror is None:
onerror = handle_remove_readonly
try:
shutil.rmtree(
directory, ignore_errors=ignore_errors, onerror=onerror
)
shutil.rmtree(directory, ignore_errors=ignore_errors, onerror=onerror)
except (IOError, OSError, FileNotFoundError) as exc:
# Ignore removal failures where the file doesn't exist
if exc.errno != errno.ENOENT:
@@ -355,14 +357,10 @@ def handle_remove_readonly(func, path, exc):
:func:`set_write_bit` on the target path and try again.
"""
# Check for read-only attribute
from .compat import (
ResourceWarning, FileNotFoundError, PermissionError
)
from .compat import ResourceWarning, FileNotFoundError, PermissionError
PERM_ERRORS = (errno.EACCES, errno.EPERM, errno.ENOENT)
default_warning_message = (
"Unable to remove file due to permissions restriction: {!r}"
)
default_warning_message = "Unable to remove file due to permissions restriction: {!r}"
# split the initial exception out into its type, exception, and traceback
exc_type, exc_exception, exc_tb = exc
if is_readonly_path(path):
@@ -488,8 +486,8 @@ def get_converted_relative_path(path, relative_to=None):
raise ValueError("The path argument does not currently accept UNC paths")
relpath_s = to_text(posixpath.normpath(path.as_posix()))
if not (relpath_s == u"." or relpath_s.startswith(u"./")):
relpath_s = posixpath.join(u".", relpath_s)
if not (relpath_s == "." or relpath_s.startswith("./")):
relpath_s = posixpath.join(".", relpath_s)
return relpath_s
+14 -18
View File
@@ -7,30 +7,31 @@ import signal
import sys
import threading
import time
from io import StringIO
import colorama
import six
from .compat import to_native_string
from .termcolors import COLOR_MAP, COLORS, colored, DISABLE_COLORS
from .cursor import hide_cursor, show_cursor
from .misc import decode_for_output
from io import StringIO
from .termcolors import COLOR_MAP, COLORS, DISABLE_COLORS, colored
try:
import yaspin
import cursor
except ImportError:
yaspin = None
Spinners = None
SpinBase = None
cursor = None
else:
import yaspin.spinners
import yaspin.core
Spinners = yaspin.spinners.Spinners
SpinBase = yaspin.core.Yaspin
if os.name == "nt":
def handler(signum, frame, spinner):
"""Signal handler, used to gracefully shut down the ``spinner`` instance
when specified signal is received by the process running the ``spinner``.
@@ -42,7 +43,9 @@ if os.name == "nt":
spinner.stop()
sys.exit(0)
else:
def handler(signum, frame, spinner):
"""Signal handler, used to gracefully shut down the ``spinner`` instance
when specified signal is received by the process running the ``spinner``.
@@ -54,12 +57,10 @@ else:
spinner.stop()
sys.exit(0)
CLEAR_LINE = chr(27) + "[K"
TRANSLATION_MAP = {
10004: u"OK",
10008: u"x",
}
TRANSLATION_MAP = {10004: u"OK", 10008: u"x"}
decode_output = functools.partial(decode_for_output, translation_map=TRANSLATION_MAP)
@@ -85,6 +86,7 @@ class DummySpinner(object):
def __exit__(self, exc_type, exc_val, tb):
if exc_type:
import traceback
formatted_tb = traceback.format_exception(exc_type, exc_val, tb)
self.write_err("".join(formatted_tb))
self._close_output_buffer()
@@ -198,10 +200,7 @@ class VistirSpinner(SpinBase):
colorama.init()
sigmap = {}
if handler:
sigmap.update({
signal.SIGINT: handler,
signal.SIGTERM: handler
})
sigmap.update({signal.SIGINT: handler, signal.SIGTERM: handler})
handler_map = kwargs.pop("handler_map", {})
if os.name == "nt":
sigmap[signal.SIGBREAK] = handler
@@ -333,10 +332,7 @@ class VistirSpinner(SpinBase):
def _compose_color_func(self):
fn = functools.partial(
colored,
color=self._color,
on_color=self._on_color,
attrs=list(self._attrs),
colored, color=self._color, on_color=self._on_color, attrs=list(self._attrs)
)
return fn
@@ -421,13 +417,13 @@ class VistirSpinner(SpinBase):
def _hide_cursor(target=None):
if not target:
target = sys.stdout
cursor.hide(stream=target)
hide_cursor(stream=target)
@staticmethod
def _show_cursor(target=None):
if not target:
target = sys.stdout
cursor.show(stream=target)
show_cursor(stream=target)
@staticmethod
def _clear_err():
+37 -47
View File
@@ -1,62 +1,55 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import colorama
import os
import colorama
from .compat import to_native_string
DISABLE_COLORS = os.getenv("CI", False) or os.getenv("ANSI_COLORS_DISABLED",
os.getenv("VISTIR_DISABLE_COLORS", False)
DISABLE_COLORS = os.getenv("CI", False) or os.getenv(
"ANSI_COLORS_DISABLED", os.getenv("VISTIR_DISABLE_COLORS", False)
)
ATTRIBUTES = dict(
list(zip([
'bold',
'dark',
'',
'underline',
'blink',
'',
'reverse',
'concealed'
],
list(range(1, 9))
))
list(
zip(
["bold", "dark", "", "underline", "blink", "", "reverse", "concealed"],
list(range(1, 9)),
)
del ATTRIBUTES['']
)
)
del ATTRIBUTES[""]
HIGHLIGHTS = dict(
list(zip([
'on_grey',
'on_red',
'on_green',
'on_yellow',
'on_blue',
'on_magenta',
'on_cyan',
'on_white'
list(
zip(
[
"on_grey",
"on_red",
"on_green",
"on_yellow",
"on_blue",
"on_magenta",
"on_cyan",
"on_white",
],
list(range(40, 48))
))
list(range(40, 48)),
)
)
)
COLORS = dict(
list(zip([
'grey',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
],
list(range(30, 38))
))
list(
zip(
["grey", "red", "green", "yellow", "blue", "magenta", "cyan", "white"],
list(range(30, 38)),
)
)
)
COLOR_MAP = {
@@ -106,11 +99,11 @@ def colored(text, color=None, on_color=None, attrs=None):
colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink'])
colored('Hello, World!', 'green')
"""
if os.getenv('ANSI_COLORS_DISABLED') is None:
if os.getenv("ANSI_COLORS_DISABLED") is None:
style = "NORMAL"
if 'bold' in attrs:
if "bold" in attrs:
style = "BRIGHT"
attrs.remove('bold')
attrs.remove("bold")
if color is not None:
color = color.upper()
text = to_native_string("%s%s%s%s%s") % (
@@ -131,10 +124,7 @@ def colored(text, color=None, on_color=None, attrs=None):
)
if attrs is not None:
fmt_str = to_native_string("%s[%%dm%%s%s[9m") % (
chr(27),
chr(27)
)
fmt_str = to_native_string("%s[%%dm%%s%s[9m") % (chr(27), chr(27))
for attr in attrs:
text = fmt_str % (ATTRIBUTES[attr], text)
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.14.0"
__version__ = "0.14.1"
+4 -2
View File
@@ -84,5 +84,7 @@ def kbi_safe_yaspin(*args, **kwargs):
return Yaspin(*args, **kwargs)
_kbi_safe_doc = yaspin.__doc__.replace("yaspin", "kbi_safe_yaspin")
kbi_safe_yaspin.__doc__ = _kbi_safe_doc
# Handle PYTHONOPTIMIZE=2 case, when docstrings are set to None.
if yaspin.__doc__:
_kbi_safe_doc = yaspin.__doc__.replace("yaspin", "kbi_safe_yaspin")
kbi_safe_yaspin.__doc__ = _kbi_safe_doc
+3 -3
View File
@@ -17,7 +17,7 @@ import threading
import time
import colorama
import cursor
from pipenv.vendor.vistir import cursor
from .base_spinner import default_spinner
from .compat import PY2, basestring, builtin_str, bytes, iteritems, str
@@ -530,11 +530,11 @@ class Yaspin(object):
@staticmethod
def _hide_cursor():
cursor.hide()
cursor.hide_cursor()
@staticmethod
def _show_cursor():
cursor.show()
cursor.show_cursor()
@staticmethod
def _clear_line():
+3 -2
View File
@@ -128,10 +128,11 @@ setup(
],
},
python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*",
setup_requires=["invoke", "parver"],
setup_requires=["invoke", "parver", ],
install_requires=required,
extras_require={
"test": ["pytest<4.0", "pytest-tap", "pytest-xdist", "flaky", "mock"]
"test": ["pytest<4.0", "pytest-tap", "pytest-xdist", "flaky", "mock"],
"dev": ["towncrier", "bs4"],
},
include_package_data=True,
license="MIT",
+6 -4
View File
@@ -15,21 +15,23 @@ diff --git a/pipenv/vendor/vistir/compat.py b/pipenv/vendor/vistir/compat.py
index 9ae33fdc..ec3b65cb 100644
--- a/pipenv/vendor/vistir/compat.py
+++ b/pipenv/vendor/vistir/compat.py
@@ -43,11 +43,11 @@ if sys.version_info >= (3, 5):
@@ -43,12 +43,12 @@ if sys.version_info >= (3, 5):
from functools import lru_cache
else:
from pathlib2 import Path
- from pathlib2 import Path
- from backports.functools_lru_cache import lru_cache
+ from pipenv.vendor.pathlib2 import Path
+ from pipenv.vendor.backports.functools_lru_cache import lru_cache
from .backports.tempfile import NamedTemporaryFile as _NamedTemporaryFile
if sys.version_info < (3, 3):
- from backports.shutil_get_terminal_size import get_terminal_size
+ from pipenv.vendor.backports.shutil_get_terminal_size import get_terminal_size
NamedTemporaryFile = _NamedTemporaryFile
else:
from tempfile import NamedTemporaryFile
@@ -56,7 +56,7 @@ else:
@@ -57,7 +57,7 @@ else:
try:
from weakref import finalize
except ImportError:
@@ -7,7 +7,7 @@ index d01fb98e..06b8b621 100644
import time
+import colorama
+import cursor
+from pipenv.vendor.vistir import cursor
+
from .base_spinner import default_spinner
from .compat import PY2, basestring, builtin_str, bytes, iteritems, str
@@ -48,13 +48,13 @@ index d01fb98e..06b8b621 100644
def _hide_cursor():
- sys.stdout.write("\033[?25l")
- sys.stdout.flush()
+ cursor.hide()
+ cursor.hide_cursor()
@staticmethod
def _show_cursor():
- sys.stdout.write("\033[?25h")
- sys.stdout.flush()
+ cursor.show()
+ cursor.show_cursor()
@staticmethod
def _clear_line():