mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 22:50:18 +00:00
Update all vendored dependencies
Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
@@ -19,8 +19,8 @@ from pipenv.patched.notpip._internal.exceptions import (
|
||||
UnsupportedPythonVersion,
|
||||
)
|
||||
from pipenv.patched.notpip._internal.req.constructors import install_req_from_req_string
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.req.req_install import InstallRequirement
|
||||
from pipenv.patched.notpip._internal.utils.logging import indent_log
|
||||
from pipenv.patched.notpip._internal.utils.misc import dist_in_usersite, ensure_dir
|
||||
from pipenv.patched.notpip._internal.utils.packaging import check_dist_requires_python
|
||||
from pipenv.patched.notpip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
@@ -14,7 +14,7 @@ from packaging.requirements import Requirement
|
||||
from packaging.specifiers import SpecifierSet, Specifier
|
||||
|
||||
os.environ["PIP_SHIMS_BASE_MODULE"] = str("pipenv.patched.notpip")
|
||||
from pip_shims.shims import VcsSupport, WheelCache, InstallationError, pip_version
|
||||
from pip_shims.shims import VcsSupport, WheelCache, InstallationError
|
||||
from pip_shims.shims import Resolver as PipResolver
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class PyPIRepository(BaseRepository):
|
||||
}
|
||||
|
||||
# pip 19.0 has removed process_dependency_links from the PackageFinder constructor
|
||||
if pkg_resources.parse_version(pip_version) < pkg_resources.parse_version('19.0'):
|
||||
if pkg_resources.parse_version(pip_shims.shims.pip_version) < pkg_resources.parse_version('19.0'):
|
||||
finder_kwargs["process_dependency_links"] = pip_options.process_dependency_links
|
||||
|
||||
self.finder = PackageFinder(**finder_kwargs)
|
||||
@@ -279,7 +279,7 @@ class PyPIRepository(BaseRepository):
|
||||
'finder': self.finder,
|
||||
'session': self.session,
|
||||
'upgrade_strategy': "to-satisfy-only",
|
||||
'force_reinstall': True,
|
||||
'force_reinstall': False,
|
||||
'ignore_dependencies': False,
|
||||
'ignore_requires_python': True,
|
||||
'ignore_installed': True,
|
||||
@@ -309,6 +309,7 @@ class PyPIRepository(BaseRepository):
|
||||
cleanup_fn()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
results = set(results) if results else set()
|
||||
return results, ireq
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
from . import weakref
|
||||
from . import enum
|
||||
from . import shutil_get_terminal_size
|
||||
from . import enum
|
||||
from . import functools_lru_cache
|
||||
|
||||
Vendored
+9
-6
@@ -10,20 +10,23 @@
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from pkg_resources import get_distribution, DistributionNotFound
|
||||
|
||||
from cerberus.validator import DocumentError, Validator
|
||||
from cerberus.schema import (rules_set_registry, schema_registry, Registry,
|
||||
SchemaError)
|
||||
from cerberus.schema import rules_set_registry, schema_registry, SchemaError
|
||||
from cerberus.utils import TypeDefinition
|
||||
|
||||
|
||||
__version__ = "1.2"
|
||||
try:
|
||||
__version__ = get_distribution("Cerberus").version
|
||||
except DistributionNotFound:
|
||||
__version__ = "unknown"
|
||||
|
||||
__all__ = [
|
||||
DocumentError.__name__,
|
||||
Registry.__name__,
|
||||
SchemaError.__name__,
|
||||
TypeDefinition.__name__,
|
||||
Validator.__name__,
|
||||
'schema_registry',
|
||||
'rules_set_registry'
|
||||
"schema_registry",
|
||||
"rules_set_registry",
|
||||
]
|
||||
|
||||
Vendored
+105
-97
@@ -3,12 +3,12 @@
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from collections import defaultdict, namedtuple, MutableMapping
|
||||
from collections import defaultdict, namedtuple
|
||||
from copy import copy, deepcopy
|
||||
from functools import wraps
|
||||
from pprint import pformat
|
||||
|
||||
from cerberus.platform import PYTHON_VERSION
|
||||
from cerberus.platform import PYTHON_VERSION, MutableMapping
|
||||
from cerberus.utils import compare_paths_lt, quote_string
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ UNALLOWED_VALUE = ErrorDefinition(0x44, 'allowed')
|
||||
UNALLOWED_VALUES = ErrorDefinition(0x45, 'allowed')
|
||||
FORBIDDEN_VALUE = ErrorDefinition(0x46, 'forbidden')
|
||||
FORBIDDEN_VALUES = ErrorDefinition(0x47, 'forbidden')
|
||||
MISSING_MEMBERS = ErrorDefinition(0x48, 'contains')
|
||||
|
||||
# other
|
||||
NORMALIZATION = ErrorDefinition(0x60, None)
|
||||
@@ -66,9 +67,10 @@ SETTING_DEFAULT_FAILED = ErrorDefinition(0x64, 'default_setter')
|
||||
ERROR_GROUP = ErrorDefinition(0x80, None)
|
||||
MAPPING_SCHEMA = ErrorDefinition(0x81, 'schema')
|
||||
SEQUENCE_SCHEMA = ErrorDefinition(0x82, 'schema')
|
||||
KEYSCHEMA = ErrorDefinition(0x83, 'keyschema')
|
||||
VALUESCHEMA = ErrorDefinition(0x84, 'valueschema')
|
||||
BAD_ITEMS = ErrorDefinition(0x8f, 'items')
|
||||
# TODO remove KEYSCHEMA AND VALUESCHEMA with next major release
|
||||
KEYSRULES = KEYSCHEMA = ErrorDefinition(0x83, 'keysrules')
|
||||
VALUESRULES = VALUESCHEMA = ErrorDefinition(0x84, 'valuesrules')
|
||||
BAD_ITEMS = ErrorDefinition(0x8F, 'items')
|
||||
|
||||
LOGICAL = ErrorDefinition(0x90, None)
|
||||
NONEOF = ErrorDefinition(0x91, 'noneof')
|
||||
@@ -79,8 +81,7 @@ ALLOF = ErrorDefinition(0x94, 'allof')
|
||||
|
||||
""" SchemaError messages """
|
||||
|
||||
SCHEMA_ERROR_DEFINITION_TYPE = \
|
||||
"schema definition for field '{0}' must be a dict"
|
||||
SCHEMA_ERROR_DEFINITION_TYPE = "schema definition for field '{0}' must be a dict"
|
||||
SCHEMA_ERROR_MISSING = "validation schema missing"
|
||||
|
||||
|
||||
@@ -89,8 +90,8 @@ SCHEMA_ERROR_MISSING = "validation schema missing"
|
||||
|
||||
class ValidationError(object):
|
||||
""" A simple class to store and query basic error information. """
|
||||
def __init__(self, document_path, schema_path, code, rule, constraint,
|
||||
value, info):
|
||||
|
||||
def __init__(self, document_path, schema_path, code, rule, constraint, value, info):
|
||||
self.document_path = document_path
|
||||
""" The path to the field within the document that caused the error.
|
||||
Type: :class:`tuple` """
|
||||
@@ -115,8 +116,7 @@ class ValidationError(object):
|
||||
|
||||
def __hash__(self):
|
||||
""" Expects that all other properties are transitively determined. """
|
||||
return hash(self.document_path) ^ hash(self.schema_path) \
|
||||
^ hash(self.code)
|
||||
return hash(self.document_path) ^ hash(self.schema_path) ^ hash(self.code)
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.document_path != other.document_path:
|
||||
@@ -125,20 +125,24 @@ class ValidationError(object):
|
||||
return compare_paths_lt(self.schema_path, other.schema_path)
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name} @ {memptr} ( " \
|
||||
"document_path={document_path}," \
|
||||
"schema_path={schema_path}," \
|
||||
"code={code}," \
|
||||
"constraint={constraint}," \
|
||||
"value={value}," \
|
||||
"info={info} )"\
|
||||
.format(class_name=self.__class__.__name__, memptr=hex(id(self)), # noqa: E501
|
||||
document_path=self.document_path,
|
||||
schema_path=self.schema_path,
|
||||
code=hex(self.code),
|
||||
constraint=quote_string(self.constraint),
|
||||
value=quote_string(self.value),
|
||||
info=self.info)
|
||||
return (
|
||||
"{class_name} @ {memptr} ( "
|
||||
"document_path={document_path},"
|
||||
"schema_path={schema_path},"
|
||||
"code={code},"
|
||||
"constraint={constraint},"
|
||||
"value={value},"
|
||||
"info={info} )".format(
|
||||
class_name=self.__class__.__name__,
|
||||
memptr=hex(id(self)), # noqa: E501
|
||||
document_path=self.document_path,
|
||||
schema_path=self.schema_path,
|
||||
code=hex(self.code),
|
||||
constraint=quote_string(self.constraint),
|
||||
value=quote_string(self.value),
|
||||
info=self.info,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def child_errors(self):
|
||||
@@ -190,11 +194,13 @@ class ErrorList(list):
|
||||
""" A list for :class:`~cerberus.errors.ValidationError` instances that
|
||||
can be queried with the ``in`` keyword for a particular
|
||||
:class:`~cerberus.errors.ErrorDefinition`. """
|
||||
|
||||
def __contains__(self, error_definition):
|
||||
for code in (x.code for x in self):
|
||||
if code == error_definition.code:
|
||||
return True
|
||||
return False
|
||||
if not isinstance(error_definition, ErrorDefinition):
|
||||
raise TypeError
|
||||
|
||||
wanted_code = error_definition.code
|
||||
return any(x.code == wanted_code for x in self)
|
||||
|
||||
|
||||
class ErrorTreeNode(MutableMapping):
|
||||
@@ -203,14 +209,10 @@ class ErrorTreeNode(MutableMapping):
|
||||
def __init__(self, path, parent_node):
|
||||
self.parent_node = parent_node
|
||||
self.tree_root = self.parent_node.tree_root
|
||||
self.path = path[:self.parent_node.depth + 1]
|
||||
self.path = path[: self.parent_node.depth + 1]
|
||||
self.errors = ErrorList()
|
||||
self.descendants = {}
|
||||
|
||||
def __add__(self, error):
|
||||
self.add(error)
|
||||
return self
|
||||
|
||||
def __contains__(self, item):
|
||||
if isinstance(item, ErrorDefinition):
|
||||
return item in self.errors
|
||||
@@ -228,6 +230,7 @@ class ErrorTreeNode(MutableMapping):
|
||||
for error in self.errors:
|
||||
if item.code == error.code:
|
||||
return error
|
||||
return None
|
||||
else:
|
||||
return self.descendants.get(item)
|
||||
|
||||
@@ -258,14 +261,16 @@ class ErrorTreeNode(MutableMapping):
|
||||
if key not in self.descendants:
|
||||
self[key] = ErrorTreeNode(error_path, self)
|
||||
|
||||
node = self[key]
|
||||
|
||||
if len(error_path) == self.depth + 1:
|
||||
self[key].errors.append(error)
|
||||
self[key].errors.sort()
|
||||
node.errors.append(error)
|
||||
node.errors.sort()
|
||||
if error.is_group_error:
|
||||
for child_error in error.child_errors:
|
||||
self.tree_root += child_error
|
||||
self.tree_root.add(child_error)
|
||||
else:
|
||||
self[key] += error
|
||||
node.add(error)
|
||||
|
||||
def _path_of_(self, error):
|
||||
return getattr(error, self.tree_type + '_path')
|
||||
@@ -274,14 +279,15 @@ class ErrorTreeNode(MutableMapping):
|
||||
class ErrorTree(ErrorTreeNode):
|
||||
""" Base class for :class:`~cerberus.errors.DocumentErrorTree` and
|
||||
:class:`~cerberus.errors.SchemaErrorTree`. """
|
||||
def __init__(self, errors=[]):
|
||||
|
||||
def __init__(self, errors=()):
|
||||
self.parent_node = None
|
||||
self.tree_root = self
|
||||
self.path = ()
|
||||
self.errors = ErrorList()
|
||||
self.descendants = {}
|
||||
for error in errors:
|
||||
self += error
|
||||
self.add(error)
|
||||
|
||||
def add(self, error):
|
||||
""" Add an error to the tree.
|
||||
@@ -323,18 +329,21 @@ class ErrorTree(ErrorTreeNode):
|
||||
class DocumentErrorTree(ErrorTree):
|
||||
""" Implements a dict-like class to query errors by indexes following the
|
||||
structure of a validated document. """
|
||||
|
||||
tree_type = 'document'
|
||||
|
||||
|
||||
class SchemaErrorTree(ErrorTree):
|
||||
""" Implements a dict-like class to query errors by indexes following the
|
||||
structure of the used schema. """
|
||||
|
||||
tree_type = 'schema'
|
||||
|
||||
|
||||
class BaseErrorHandler(object):
|
||||
""" Base class for all error handlers.
|
||||
Subclasses are identified as error-handlers with an instance-test. """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Optionally initialize a new instance. """
|
||||
pass
|
||||
@@ -411,9 +420,9 @@ def encode_unicode(f):
|
||||
This decorator ensures that if legacy Python is used unicode
|
||||
strings are encoded before passing to a function.
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def wrapped(obj, error):
|
||||
|
||||
def _encode(value):
|
||||
"""Helper encoding unicode strings into binary utf-8"""
|
||||
if isinstance(value, unicode): # noqa: F821
|
||||
@@ -436,56 +445,52 @@ class BasicErrorHandler(BaseErrorHandler):
|
||||
through :class:`str` a pretty-formatted representation of that
|
||||
tree is returned.
|
||||
"""
|
||||
messages = {0x00: "{0}",
|
||||
|
||||
0x01: "document is missing",
|
||||
0x02: "required field",
|
||||
0x03: "unknown field",
|
||||
0x04: "field '{0}' is required",
|
||||
0x05: "depends on these values: {constraint}",
|
||||
0x06: "{0} must not be present with '{field}'",
|
||||
|
||||
0x21: "'{0}' is not a document, must be a dict",
|
||||
0x22: "empty values not allowed",
|
||||
0x23: "null value not allowed",
|
||||
0x24: "must be of {constraint} type",
|
||||
0x25: "must be of dict type",
|
||||
0x26: "length of list should be {constraint}, it is {0}",
|
||||
0x27: "min length is {constraint}",
|
||||
0x28: "max length is {constraint}",
|
||||
|
||||
0x41: "value does not match regex '{constraint}'",
|
||||
0x42: "min value is {constraint}",
|
||||
0x43: "max value is {constraint}",
|
||||
0x44: "unallowed value {value}",
|
||||
0x45: "unallowed values {0}",
|
||||
0x46: "unallowed value {value}",
|
||||
0x47: "unallowed values {0}",
|
||||
|
||||
0x61: "field '{field}' cannot be coerced: {0}",
|
||||
0x62: "field '{field}' cannot be renamed: {0}",
|
||||
0x63: "field is read-only",
|
||||
0x64: "default value for '{field}' cannot be set: {0}",
|
||||
|
||||
0x81: "mapping doesn't validate subschema: {0}",
|
||||
0x82: "one or more sequence-items don't validate: {0}",
|
||||
0x83: "one or more keys of a mapping don't validate: {0}",
|
||||
0x84: "one or more values in a mapping don't validate: {0}",
|
||||
0x85: "one or more sequence-items don't validate: {0}",
|
||||
|
||||
0x91: "one or more definitions validate",
|
||||
0x92: "none or more than one rule validate",
|
||||
0x93: "no definitions validate",
|
||||
0x94: "one or more definitions don't validate"
|
||||
}
|
||||
messages = {
|
||||
0x00: "{0}",
|
||||
0x01: "document is missing",
|
||||
0x02: "required field",
|
||||
0x03: "unknown field",
|
||||
0x04: "field '{0}' is required",
|
||||
0x05: "depends on these values: {constraint}",
|
||||
0x06: "{0} must not be present with '{field}'",
|
||||
0x21: "'{0}' is not a document, must be a dict",
|
||||
0x22: "empty values not allowed",
|
||||
0x23: "null value not allowed",
|
||||
0x24: "must be of {constraint} type",
|
||||
0x25: "must be of dict type",
|
||||
0x26: "length of list should be {constraint}, it is {0}",
|
||||
0x27: "min length is {constraint}",
|
||||
0x28: "max length is {constraint}",
|
||||
0x41: "value does not match regex '{constraint}'",
|
||||
0x42: "min value is {constraint}",
|
||||
0x43: "max value is {constraint}",
|
||||
0x44: "unallowed value {value}",
|
||||
0x45: "unallowed values {0}",
|
||||
0x46: "unallowed value {value}",
|
||||
0x47: "unallowed values {0}",
|
||||
0x48: "missing members {0}",
|
||||
0x61: "field '{field}' cannot be coerced: {0}",
|
||||
0x62: "field '{field}' cannot be renamed: {0}",
|
||||
0x63: "field is read-only",
|
||||
0x64: "default value for '{field}' cannot be set: {0}",
|
||||
0x81: "mapping doesn't validate subschema: {0}",
|
||||
0x82: "one or more sequence-items don't validate: {0}",
|
||||
0x83: "one or more keys of a mapping don't validate: {0}",
|
||||
0x84: "one or more values in a mapping don't validate: {0}",
|
||||
0x85: "one or more sequence-items don't validate: {0}",
|
||||
0x91: "one or more definitions validate",
|
||||
0x92: "none or more than one rule validate",
|
||||
0x93: "no definitions validate",
|
||||
0x94: "one or more definitions don't validate",
|
||||
}
|
||||
|
||||
def __init__(self, tree=None):
|
||||
self.tree = {} if tree is None else tree
|
||||
|
||||
def __call__(self, errors=None):
|
||||
if errors is not None:
|
||||
self.clear()
|
||||
self.extend(errors)
|
||||
def __call__(self, errors):
|
||||
self.clear()
|
||||
self.extend(errors)
|
||||
return self.pretty_tree
|
||||
|
||||
def __str__(self):
|
||||
@@ -511,8 +516,9 @@ class BasicErrorHandler(BaseErrorHandler):
|
||||
elif error.is_group_error:
|
||||
self._insert_group_error(error)
|
||||
elif error.code in self.messages:
|
||||
self._insert_error(error.document_path,
|
||||
self._format_message(error.field, error))
|
||||
self._insert_error(
|
||||
error.document_path, self._format_message(error.field, error)
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
self.tree = {}
|
||||
@@ -522,8 +528,8 @@ class BasicErrorHandler(BaseErrorHandler):
|
||||
|
||||
def _format_message(self, field, error):
|
||||
return self.messages[error.code].format(
|
||||
*error.info, constraint=error.constraint,
|
||||
field=field, value=error.value)
|
||||
*error.info, constraint=error.constraint, field=field, value=error.value
|
||||
)
|
||||
|
||||
def _insert_error(self, path, node):
|
||||
""" Adds an error or sub-tree to :attr:tree.
|
||||
@@ -559,14 +565,14 @@ class BasicErrorHandler(BaseErrorHandler):
|
||||
elif child_error.is_group_error:
|
||||
self._insert_group_error(child_error)
|
||||
else:
|
||||
self._insert_error(child_error.document_path,
|
||||
self._format_message(child_error.field,
|
||||
child_error))
|
||||
self._insert_error(
|
||||
child_error.document_path,
|
||||
self._format_message(child_error.field, child_error),
|
||||
)
|
||||
|
||||
def _insert_logic_error(self, error):
|
||||
field = error.field
|
||||
self._insert_error(error.document_path,
|
||||
self._format_message(field, error))
|
||||
self._insert_error(error.document_path, self._format_message(field, error))
|
||||
|
||||
for definition_errors in error.definitions_errors.values():
|
||||
for child_error in definition_errors:
|
||||
@@ -575,8 +581,10 @@ class BasicErrorHandler(BaseErrorHandler):
|
||||
elif child_error.is_group_error:
|
||||
self._insert_group_error(child_error)
|
||||
else:
|
||||
self._insert_error(child_error.document_path,
|
||||
self._format_message(field, child_error))
|
||||
self._insert_error(
|
||||
child_error.document_path,
|
||||
self._format_message(field, child_error),
|
||||
)
|
||||
|
||||
def _purge_empty_dicts(self, error_list):
|
||||
subtree = error_list[-1]
|
||||
|
||||
Vendored
+26
@@ -12,3 +12,29 @@ if PYTHON_VERSION < 3:
|
||||
else:
|
||||
_str_type = str
|
||||
_int_types = (int,)
|
||||
|
||||
|
||||
if PYTHON_VERSION < 3.3:
|
||||
from collections import ( # noqa: F401
|
||||
Callable,
|
||||
Container,
|
||||
Hashable,
|
||||
Iterable,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Sequence,
|
||||
Set,
|
||||
Sized,
|
||||
)
|
||||
else:
|
||||
from collections.abc import ( # noqa: F401
|
||||
Callable,
|
||||
Container,
|
||||
Hashable,
|
||||
Iterable,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Sequence,
|
||||
Set,
|
||||
Sized,
|
||||
)
|
||||
|
||||
Vendored
+176
-125
@@ -1,13 +1,23 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from collections import (Callable, Hashable, Iterable, Mapping,
|
||||
MutableMapping, Sequence)
|
||||
from copy import copy
|
||||
from warnings import warn
|
||||
|
||||
from cerberus import errors
|
||||
from cerberus.platform import _str_type
|
||||
from cerberus.utils import (get_Validator_class, validator_factory,
|
||||
mapping_hash, TypeDefinition)
|
||||
from cerberus.platform import (
|
||||
_str_type,
|
||||
Callable,
|
||||
Hashable,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Sequence,
|
||||
)
|
||||
from cerberus.utils import (
|
||||
get_Validator_class,
|
||||
validator_factory,
|
||||
mapping_hash,
|
||||
TypeDefinition,
|
||||
)
|
||||
|
||||
|
||||
class _Abort(Exception):
|
||||
@@ -17,6 +27,7 @@ class _Abort(Exception):
|
||||
class SchemaError(Exception):
|
||||
""" Raised when the validation schema is missing, has the wrong format or
|
||||
contains errors. """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -26,18 +37,19 @@ class DefinitionSchema(MutableMapping):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if 'SchemaValidator' not in globals():
|
||||
global SchemaValidator
|
||||
SchemaValidator = validator_factory('SchemaValidator',
|
||||
SchemaValidatorMixin)
|
||||
SchemaValidator = validator_factory('SchemaValidator', SchemaValidatorMixin)
|
||||
types_mapping = SchemaValidator.types_mapping.copy()
|
||||
types_mapping.update({
|
||||
'callable': TypeDefinition('callable', (Callable,), ()),
|
||||
'hashable': TypeDefinition('hashable', (Hashable,), ())
|
||||
})
|
||||
types_mapping.update(
|
||||
{
|
||||
'callable': TypeDefinition('callable', (Callable,), ()),
|
||||
'hashable': TypeDefinition('hashable', (Hashable,), ()),
|
||||
}
|
||||
)
|
||||
SchemaValidator.types_mapping = types_mapping
|
||||
|
||||
return super(DefinitionSchema, cls).__new__(cls)
|
||||
|
||||
def __init__(self, validator, schema={}):
|
||||
def __init__(self, validator, schema):
|
||||
"""
|
||||
:param validator: An instance of Validator-(sub-)class that uses this
|
||||
schema.
|
||||
@@ -45,8 +57,7 @@ class DefinitionSchema(MutableMapping):
|
||||
one.
|
||||
"""
|
||||
if not isinstance(validator, get_Validator_class()):
|
||||
raise RuntimeError('validator argument must be a Validator-'
|
||||
'instance.')
|
||||
raise RuntimeError('validator argument must be a Validator-' 'instance.')
|
||||
self.validator = validator
|
||||
|
||||
if isinstance(schema, _str_type):
|
||||
@@ -56,14 +67,16 @@ class DefinitionSchema(MutableMapping):
|
||||
try:
|
||||
schema = dict(schema)
|
||||
except Exception:
|
||||
raise SchemaError(
|
||||
errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema))
|
||||
raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema))
|
||||
|
||||
self.validation_schema = SchemaValidationSchema(validator)
|
||||
self.schema_validator = SchemaValidator(
|
||||
None, allow_unknown=self.validation_schema,
|
||||
None,
|
||||
allow_unknown=self.validation_schema,
|
||||
error_handler=errors.SchemaErrorHandler,
|
||||
target_schema=schema, target_validator=validator)
|
||||
target_schema=schema,
|
||||
target_validator=validator,
|
||||
)
|
||||
|
||||
schema = self.expand(schema)
|
||||
self.validate(schema)
|
||||
@@ -110,6 +123,10 @@ class DefinitionSchema(MutableMapping):
|
||||
schema = cls._expand_subschemas(schema)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# TODO remove this with the next major release
|
||||
schema = cls._rename_deprecated_rulenames(schema)
|
||||
|
||||
return schema
|
||||
|
||||
@classmethod
|
||||
@@ -119,13 +136,15 @@ class DefinitionSchema(MutableMapping):
|
||||
:param schema: The schema-definition to expand.
|
||||
:return: The expanded schema-definition.
|
||||
"""
|
||||
|
||||
def is_of_rule(x):
|
||||
return isinstance(x, _str_type) and \
|
||||
x.startswith(('allof_', 'anyof_', 'noneof_', 'oneof_'))
|
||||
return isinstance(x, _str_type) and x.startswith(
|
||||
('allof_', 'anyof_', 'noneof_', 'oneof_')
|
||||
)
|
||||
|
||||
for field in schema:
|
||||
for of_rule in (x for x in schema[field] if is_of_rule(x)):
|
||||
operator, rule = of_rule.split('_')
|
||||
operator, rule = of_rule.split('_', 1)
|
||||
schema[field].update({operator: []})
|
||||
for value in schema[field][of_rule]:
|
||||
schema[field][operator].append({rule: value})
|
||||
@@ -135,15 +154,15 @@ class DefinitionSchema(MutableMapping):
|
||||
@classmethod
|
||||
def _expand_subschemas(cls, schema):
|
||||
def has_schema_rule():
|
||||
return isinstance(schema[field], Mapping) and \
|
||||
'schema' in schema[field]
|
||||
return isinstance(schema[field], Mapping) and 'schema' in schema[field]
|
||||
|
||||
def has_mapping_schema():
|
||||
""" Tries to determine heuristically if the schema-constraints are
|
||||
aimed to mappings. """
|
||||
try:
|
||||
return all(isinstance(x, Mapping) for x
|
||||
in schema[field]['schema'].values())
|
||||
return all(
|
||||
isinstance(x, Mapping) for x in schema[field]['schema'].values()
|
||||
)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
@@ -153,13 +172,12 @@ class DefinitionSchema(MutableMapping):
|
||||
elif has_mapping_schema():
|
||||
schema[field]['schema'] = cls.expand(schema[field]['schema'])
|
||||
else: # assumes schema-constraints for a sequence
|
||||
schema[field]['schema'] = \
|
||||
cls.expand({0: schema[field]['schema']})[0]
|
||||
schema[field]['schema'] = cls.expand({0: schema[field]['schema']})[0]
|
||||
|
||||
for rule in ('keyschema', 'valueschema'):
|
||||
# TODO remove the last two values in the tuple with the next major release
|
||||
for rule in ('keysrules', 'valuesrules', 'keyschema', 'valueschema'):
|
||||
if rule in schema[field]:
|
||||
schema[field][rule] = \
|
||||
cls.expand({0: schema[field][rule]})[0]
|
||||
schema[field][rule] = cls.expand({0: schema[field][rule]})[0]
|
||||
|
||||
for rule in ('allof', 'anyof', 'items', 'noneof', 'oneof'):
|
||||
if rule in schema[field]:
|
||||
@@ -171,6 +189,12 @@ class DefinitionSchema(MutableMapping):
|
||||
schema[field][rule] = new_rules_definition
|
||||
return schema
|
||||
|
||||
def get(self, item, default=None):
|
||||
return self.schema.get(item, default)
|
||||
|
||||
def items(self):
|
||||
return self.schema.items()
|
||||
|
||||
def update(self, schema):
|
||||
try:
|
||||
schema = self.expand(schema)
|
||||
@@ -178,31 +202,64 @@ class DefinitionSchema(MutableMapping):
|
||||
_new_schema.update(schema)
|
||||
self.validate(_new_schema)
|
||||
except ValueError:
|
||||
raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE
|
||||
.format(schema))
|
||||
raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema))
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
self.schema = _new_schema
|
||||
|
||||
# TODO remove with next major release
|
||||
@staticmethod
|
||||
def _rename_deprecated_rulenames(schema):
|
||||
for field, rules in schema.items():
|
||||
|
||||
if isinstance(rules, str): # registry reference
|
||||
continue
|
||||
|
||||
for old, new in (
|
||||
('keyschema', 'keysrules'),
|
||||
('validator', 'check_with'),
|
||||
('valueschema', 'valuesrules'),
|
||||
):
|
||||
|
||||
if old not in rules:
|
||||
continue
|
||||
|
||||
if new in rules:
|
||||
raise RuntimeError(
|
||||
"The rule '{new}' is also present with its old "
|
||||
"name '{old}' in the same set of rules."
|
||||
)
|
||||
|
||||
warn(
|
||||
"The rule '{old}' was renamed to '{new}'. The old name will "
|
||||
"not be available in the next major release of "
|
||||
"Cerberus.".format(old=old, new=new),
|
||||
DeprecationWarning,
|
||||
)
|
||||
schema[field][new] = schema[field][old]
|
||||
schema[field].pop(old)
|
||||
|
||||
return schema
|
||||
|
||||
def regenerate_validation_schema(self):
|
||||
self.validation_schema = SchemaValidationSchema(self.validator)
|
||||
|
||||
def validate(self, schema=None):
|
||||
""" Validates a schema that defines rules against supported rules.
|
||||
|
||||
:param schema: The schema to be validated as a legal cerberus schema
|
||||
according to the rules of the assigned Validator object.
|
||||
Raises a :class:`~cerberus.base.SchemaError` when an invalid
|
||||
schema is encountered. """
|
||||
if schema is None:
|
||||
schema = self.schema
|
||||
_hash = (mapping_hash(schema),
|
||||
mapping_hash(self.validator.types_mapping))
|
||||
_hash = (mapping_hash(schema), mapping_hash(self.validator.types_mapping))
|
||||
if _hash not in self.validator._valid_schemas:
|
||||
self._validate(schema)
|
||||
self.validator._valid_schemas.add(_hash)
|
||||
|
||||
def _validate(self, schema):
|
||||
""" Validates a schema that defines rules against supported rules.
|
||||
|
||||
:param schema: The schema to be validated as a legal cerberus schema
|
||||
according to the rules of this Validator object.
|
||||
"""
|
||||
if isinstance(schema, _str_type):
|
||||
schema = self.validator.schema_registry.get(schema, schema)
|
||||
|
||||
@@ -212,8 +269,7 @@ class DefinitionSchema(MutableMapping):
|
||||
schema = copy(schema)
|
||||
for field in schema:
|
||||
if isinstance(schema[field], _str_type):
|
||||
schema[field] = rules_set_registry.get(schema[field],
|
||||
schema[field])
|
||||
schema[field] = rules_set_registry.get(schema[field], schema[field])
|
||||
|
||||
if not self.schema_validator(schema, normalize=False):
|
||||
raise SchemaError(self.schema_validator.errors)
|
||||
@@ -236,31 +292,31 @@ class UnvalidatedSchema(DefinitionSchema):
|
||||
|
||||
class SchemaValidationSchema(UnvalidatedSchema):
|
||||
def __init__(self, validator):
|
||||
self.schema = {'allow_unknown': False,
|
||||
'schema': validator.rules,
|
||||
'type': 'dict'}
|
||||
self.schema = {
|
||||
'allow_unknown': False,
|
||||
'schema': validator.rules,
|
||||
'type': 'dict',
|
||||
}
|
||||
|
||||
|
||||
class SchemaValidatorMixin(object):
|
||||
""" This validator is extended to validate schemas passed to a Cerberus
|
||||
""" This validator mixin provides mechanics to validate schemas passed to a Cerberus
|
||||
validator. """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('known_rules_set_refs', set())
|
||||
kwargs.setdefault('known_schema_refs', set())
|
||||
super(SchemaValidatorMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def known_rules_set_refs(self):
|
||||
""" The encountered references to rules set registry items. """
|
||||
return self._config.get('known_rules_set_refs', ())
|
||||
|
||||
@known_rules_set_refs.setter
|
||||
def known_rules_set_refs(self, value):
|
||||
self._config['known_rules_set_refs'] = value
|
||||
return self._config['known_rules_set_refs']
|
||||
|
||||
@property
|
||||
def known_schema_refs(self):
|
||||
""" The encountered references to schema registry items. """
|
||||
return self._config.get('known_schema_refs', ())
|
||||
|
||||
@known_schema_refs.setter
|
||||
def known_schema_refs(self, value):
|
||||
self._config['known_schema_refs'] = value
|
||||
return self._config['known_schema_refs']
|
||||
|
||||
@property
|
||||
def target_schema(self):
|
||||
@@ -272,35 +328,13 @@ class SchemaValidatorMixin(object):
|
||||
""" The validator whose schema is being validated. """
|
||||
return self._config['target_validator']
|
||||
|
||||
def _validate_logical(self, rule, field, value):
|
||||
""" {'allowed': ('allof', 'anyof', 'noneof', 'oneof')} """
|
||||
if not isinstance(value, Sequence):
|
||||
self._error(field, errors.BAD_TYPE)
|
||||
return
|
||||
|
||||
validator = self._get_child_validator(
|
||||
document_crumb=rule, allow_unknown=False,
|
||||
schema=self.target_validator.validation_rules)
|
||||
|
||||
for constraints in value:
|
||||
_hash = (mapping_hash({'turing': constraints}),
|
||||
mapping_hash(self.target_validator.types_mapping))
|
||||
if _hash in self.target_validator._valid_schemas:
|
||||
continue
|
||||
|
||||
validator(constraints, normalize=False)
|
||||
if validator._errors:
|
||||
self._error(validator._errors)
|
||||
else:
|
||||
self.target_validator._valid_schemas.add(_hash)
|
||||
|
||||
def _validator_bulk_schema(self, field, value):
|
||||
def _check_with_bulk_schema(self, field, value):
|
||||
# resolve schema registry reference
|
||||
if isinstance(value, _str_type):
|
||||
if value in self.known_rules_set_refs:
|
||||
return
|
||||
else:
|
||||
self.known_rules_set_refs += (value,)
|
||||
self.known_rules_set_refs.add(value)
|
||||
definition = self.target_validator.rules_set_registry.get(value)
|
||||
if definition is None:
|
||||
self._error(field, 'Rules set definition %s not found.' % value)
|
||||
@@ -308,28 +342,32 @@ class SchemaValidatorMixin(object):
|
||||
else:
|
||||
value = definition
|
||||
|
||||
_hash = (mapping_hash({'turing': value}),
|
||||
mapping_hash(self.target_validator.types_mapping))
|
||||
_hash = (
|
||||
mapping_hash({'turing': value}),
|
||||
mapping_hash(self.target_validator.types_mapping),
|
||||
)
|
||||
if _hash in self.target_validator._valid_schemas:
|
||||
return
|
||||
|
||||
validator = self._get_child_validator(
|
||||
document_crumb=field, allow_unknown=False,
|
||||
schema=self.target_validator.rules)
|
||||
document_crumb=field,
|
||||
allow_unknown=False,
|
||||
schema=self.target_validator.rules,
|
||||
)
|
||||
validator(value, normalize=False)
|
||||
if validator._errors:
|
||||
self._error(validator._errors)
|
||||
else:
|
||||
self.target_validator._valid_schemas.add(_hash)
|
||||
|
||||
def _validator_dependencies(self, field, value):
|
||||
def _check_with_dependencies(self, field, value):
|
||||
if isinstance(value, _str_type):
|
||||
pass
|
||||
elif isinstance(value, Mapping):
|
||||
validator = self._get_child_validator(
|
||||
document_crumb=field,
|
||||
schema={'valueschema': {'type': 'list'}},
|
||||
allow_unknown=True
|
||||
schema={'valuesrules': {'type': 'list'}},
|
||||
allow_unknown=True,
|
||||
)
|
||||
if not validator(value, normalize=False):
|
||||
self._error(validator._errors)
|
||||
@@ -338,54 +376,36 @@ class SchemaValidatorMixin(object):
|
||||
path = self.document_path + (field,)
|
||||
self._error(path, 'All dependencies must be a hashable type.')
|
||||
|
||||
def _validator_handler(self, field, value):
|
||||
if isinstance(value, Callable):
|
||||
return
|
||||
if isinstance(value, _str_type):
|
||||
if value not in self.target_validator.validators + \
|
||||
self.target_validator.coercers:
|
||||
self._error(field, '%s is no valid coercer' % value)
|
||||
elif isinstance(value, Iterable):
|
||||
for handler in value:
|
||||
self._validator_handler(field, handler)
|
||||
|
||||
def _validator_items(self, field, value):
|
||||
def _check_with_items(self, field, value):
|
||||
for i, schema in enumerate(value):
|
||||
self._validator_bulk_schema((field, i), schema)
|
||||
self._check_with_bulk_schema((field, i), schema)
|
||||
|
||||
def _validator_schema(self, field, value):
|
||||
def _check_with_schema(self, field, value):
|
||||
try:
|
||||
value = self._handle_schema_reference_for_validator(field, value)
|
||||
except _Abort:
|
||||
return
|
||||
|
||||
_hash = (mapping_hash(value),
|
||||
mapping_hash(self.target_validator.types_mapping))
|
||||
_hash = (mapping_hash(value), mapping_hash(self.target_validator.types_mapping))
|
||||
if _hash in self.target_validator._valid_schemas:
|
||||
return
|
||||
|
||||
validator = self._get_child_validator(
|
||||
document_crumb=field,
|
||||
schema=None, allow_unknown=self.root_allow_unknown)
|
||||
document_crumb=field, schema=None, allow_unknown=self.root_allow_unknown
|
||||
)
|
||||
validator(self._expand_rules_set_refs(value), normalize=False)
|
||||
if validator._errors:
|
||||
self._error(validator._errors)
|
||||
else:
|
||||
self.target_validator._valid_schemas.add(_hash)
|
||||
|
||||
def _handle_schema_reference_for_validator(self, field, value):
|
||||
if not isinstance(value, _str_type):
|
||||
return value
|
||||
if value in self.known_schema_refs:
|
||||
raise _Abort
|
||||
|
||||
self.known_schema_refs += (value,)
|
||||
definition = self.target_validator.schema_registry.get(value)
|
||||
if definition is None:
|
||||
path = self.document_path + (field,)
|
||||
self._error(path, 'Schema definition {} not found.'.format(value))
|
||||
raise _Abort
|
||||
return definition
|
||||
def _check_with_type(self, field, value):
|
||||
value = set((value,)) if isinstance(value, _str_type) else set(value)
|
||||
invalid_constraints = value - set(self.target_validator.types)
|
||||
if invalid_constraints:
|
||||
self._error(
|
||||
field, 'Unsupported types: {}'.format(', '.join(invalid_constraints))
|
||||
)
|
||||
|
||||
def _expand_rules_set_refs(self, schema):
|
||||
result = {}
|
||||
@@ -396,15 +416,46 @@ class SchemaValidatorMixin(object):
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
def _validator_type(self, field, value):
|
||||
value = (value,) if isinstance(value, _str_type) else value
|
||||
invalid_constraints = ()
|
||||
for constraint in value:
|
||||
if constraint not in self.target_validator.types:
|
||||
invalid_constraints += (constraint,)
|
||||
if invalid_constraints:
|
||||
def _handle_schema_reference_for_validator(self, field, value):
|
||||
if not isinstance(value, _str_type):
|
||||
return value
|
||||
if value in self.known_schema_refs:
|
||||
raise _Abort
|
||||
|
||||
self.known_schema_refs.add(value)
|
||||
definition = self.target_validator.schema_registry.get(value)
|
||||
if definition is None:
|
||||
path = self.document_path + (field,)
|
||||
self._error(path, 'Unsupported types: %s' % invalid_constraints)
|
||||
self._error(path, 'Schema definition {} not found.'.format(value))
|
||||
raise _Abort
|
||||
return definition
|
||||
|
||||
def _validate_logical(self, rule, field, value):
|
||||
""" {'allowed': ('allof', 'anyof', 'noneof', 'oneof')} """
|
||||
if not isinstance(value, Sequence):
|
||||
self._error(field, errors.BAD_TYPE)
|
||||
return
|
||||
|
||||
validator = self._get_child_validator(
|
||||
document_crumb=rule,
|
||||
allow_unknown=False,
|
||||
schema=self.target_validator.validation_rules,
|
||||
)
|
||||
|
||||
for constraints in value:
|
||||
_hash = (
|
||||
mapping_hash({'turing': constraints}),
|
||||
mapping_hash(self.target_validator.types_mapping),
|
||||
)
|
||||
if _hash in self.target_validator._valid_schemas:
|
||||
continue
|
||||
|
||||
validator(constraints, normalize=False)
|
||||
if validator._errors:
|
||||
self._error(validator._errors)
|
||||
else:
|
||||
self.target_validator._valid_schemas.add(_hash)
|
||||
|
||||
|
||||
####
|
||||
|
||||
|
||||
+29
-14
@@ -1,22 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from cerberus import errors, Validator, SchemaError, DocumentError
|
||||
from cerberus.tests.conftest import sample_schema
|
||||
|
||||
|
||||
def assert_exception(exception, document={}, schema=None, validator=None,
|
||||
msg=None):
|
||||
def assert_exception(exception, document={}, schema=None, validator=None, msg=None):
|
||||
""" Tests whether a specific exception is raised. Optionally also tests
|
||||
whether the exception message is as expected. """
|
||||
if validator is None:
|
||||
validator = Validator()
|
||||
if msg is None:
|
||||
with pytest.raises(exception) as excinfo:
|
||||
with pytest.raises(exception):
|
||||
validator(document, schema)
|
||||
else:
|
||||
with pytest.raises(exception, message=msg) as excinfo: # noqa: F841
|
||||
with pytest.raises(exception, match=re.escape(msg)):
|
||||
validator(document, schema)
|
||||
|
||||
|
||||
@@ -32,8 +33,15 @@ def assert_document_error(*args):
|
||||
assert_exception(DocumentError, *args)
|
||||
|
||||
|
||||
def assert_fail(document, schema=None, validator=None, update=False,
|
||||
error=None, errors=None, child_errors=None):
|
||||
def assert_fail(
|
||||
document,
|
||||
schema=None,
|
||||
validator=None,
|
||||
update=False,
|
||||
error=None,
|
||||
errors=None,
|
||||
child_errors=None,
|
||||
):
|
||||
""" Tests whether a validation fails. """
|
||||
if validator is None:
|
||||
validator = Validator(sample_schema)
|
||||
@@ -45,8 +53,7 @@ def assert_fail(document, schema=None, validator=None, update=False,
|
||||
|
||||
assert not (error is not None and errors is not None)
|
||||
assert not (errors is not None and child_errors is not None), (
|
||||
'child_errors can only be tested in '
|
||||
'conjunction with the error parameter'
|
||||
'child_errors can only be tested in ' 'conjunction with the error parameter'
|
||||
)
|
||||
assert not (child_errors is not None and error is None)
|
||||
if error is not None:
|
||||
@@ -99,7 +106,8 @@ def assert_has_error(_errors, d_path, s_path, error_def, constraint, info=()):
|
||||
else:
|
||||
break
|
||||
else:
|
||||
raise AssertionError("""
|
||||
raise AssertionError(
|
||||
"""
|
||||
Error with properties:
|
||||
document_path={doc_path}
|
||||
schema_path={schema_path}
|
||||
@@ -108,9 +116,15 @@ def assert_has_error(_errors, d_path, s_path, error_def, constraint, info=()):
|
||||
info={info}
|
||||
not found in errors:
|
||||
{errors}
|
||||
""".format(doc_path=d_path, schema_path=s_path,
|
||||
code=hex(error.code), info=info,
|
||||
constraint=constraint, errors=_errors))
|
||||
""".format(
|
||||
doc_path=d_path,
|
||||
schema_path=s_path,
|
||||
code=hex(error.code),
|
||||
info=info,
|
||||
constraint=constraint,
|
||||
errors=_errors,
|
||||
)
|
||||
)
|
||||
return i
|
||||
|
||||
|
||||
@@ -133,8 +147,9 @@ def assert_not_has_error(_errors, *args, **kwargs):
|
||||
|
||||
|
||||
def assert_bad_type(field, data_type, value):
|
||||
assert_fail({field: value},
|
||||
error=(field, (field, 'type'), errors.BAD_TYPE, data_type))
|
||||
assert_fail(
|
||||
{field: value}, error=(field, (field, 'type'), errors.BAD_TYPE, data_type)
|
||||
)
|
||||
|
||||
|
||||
def assert_normalized(document, expected, schema=None, validator=None):
|
||||
|
||||
+23
-76
@@ -23,67 +23,27 @@ def validator():
|
||||
|
||||
|
||||
sample_schema = {
|
||||
'a_string': {
|
||||
'type': 'string',
|
||||
'minlength': 2,
|
||||
'maxlength': 10
|
||||
},
|
||||
'a_binary': {
|
||||
'type': 'binary',
|
||||
'minlength': 2,
|
||||
'maxlength': 10
|
||||
},
|
||||
'a_nullable_integer': {
|
||||
'type': 'integer',
|
||||
'nullable': True
|
||||
},
|
||||
'an_integer': {
|
||||
'type': 'integer',
|
||||
'min': 1,
|
||||
'max': 100,
|
||||
},
|
||||
'a_restricted_integer': {
|
||||
'type': 'integer',
|
||||
'allowed': [-1, 0, 1],
|
||||
},
|
||||
'a_boolean': {
|
||||
'type': 'boolean',
|
||||
},
|
||||
'a_datetime': {
|
||||
'type': 'datetime',
|
||||
},
|
||||
'a_float': {
|
||||
'type': 'float',
|
||||
'min': 1,
|
||||
'max': 100,
|
||||
},
|
||||
'a_number': {
|
||||
'type': 'number',
|
||||
'min': 1,
|
||||
'max': 100,
|
||||
},
|
||||
'a_set': {
|
||||
'type': 'set',
|
||||
},
|
||||
'one_or_more_strings': {
|
||||
'type': ['string', 'list'],
|
||||
'schema': {'type': 'string'}
|
||||
},
|
||||
'a_string': {'type': 'string', 'minlength': 2, 'maxlength': 10},
|
||||
'a_binary': {'type': 'binary', 'minlength': 2, 'maxlength': 10},
|
||||
'a_nullable_integer': {'type': 'integer', 'nullable': True},
|
||||
'an_integer': {'type': 'integer', 'min': 1, 'max': 100},
|
||||
'a_restricted_integer': {'type': 'integer', 'allowed': [-1, 0, 1]},
|
||||
'a_boolean': {'type': 'boolean', 'meta': 'can haz two distinct states'},
|
||||
'a_datetime': {'type': 'datetime', 'meta': {'format': '%a, %d. %b %Y'}},
|
||||
'a_float': {'type': 'float', 'min': 1, 'max': 100},
|
||||
'a_number': {'type': 'number', 'min': 1, 'max': 100},
|
||||
'a_set': {'type': 'set'},
|
||||
'one_or_more_strings': {'type': ['string', 'list'], 'schema': {'type': 'string'}},
|
||||
'a_regex_email': {
|
||||
'type': 'string',
|
||||
'regex': '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
|
||||
'regex': r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$',
|
||||
},
|
||||
'a_readonly_string': {
|
||||
'type': 'string',
|
||||
'readonly': True,
|
||||
},
|
||||
'a_restricted_string': {
|
||||
'type': 'string',
|
||||
'allowed': ["agent", "client", "vendor"],
|
||||
},
|
||||
'an_array': {
|
||||
'a_readonly_string': {'type': 'string', 'readonly': True},
|
||||
'a_restricted_string': {'type': 'string', 'allowed': ['agent', 'client', 'vendor']},
|
||||
'an_array': {'type': 'list', 'allowed': ['agent', 'client', 'vendor']},
|
||||
'an_array_from_set': {
|
||||
'type': 'list',
|
||||
'allowed': ["agent", "client", "vendor"],
|
||||
'allowed': set(['agent', 'client', 'vendor']),
|
||||
},
|
||||
'a_list_of_dicts': {
|
||||
'type': 'list',
|
||||
@@ -97,38 +57,25 @@ sample_schema = {
|
||||
},
|
||||
'a_list_of_values': {
|
||||
'type': 'list',
|
||||
'items': [{'type': 'string'}, {'type': 'integer'}, ]
|
||||
},
|
||||
'a_list_of_integers': {
|
||||
'type': 'list',
|
||||
'schema': {'type': 'integer'},
|
||||
'items': [{'type': 'string'}, {'type': 'integer'}],
|
||||
},
|
||||
'a_list_of_integers': {'type': 'list', 'schema': {'type': 'integer'}},
|
||||
'a_dict': {
|
||||
'type': 'dict',
|
||||
'schema': {
|
||||
'address': {'type': 'string'},
|
||||
'city': {'type': 'string', 'required': True}
|
||||
'city': {'type': 'string', 'required': True},
|
||||
},
|
||||
},
|
||||
'a_dict_with_valueschema': {
|
||||
'type': 'dict',
|
||||
'valueschema': {'type': 'integer'}
|
||||
},
|
||||
'a_dict_with_keyschema': {
|
||||
'type': 'dict',
|
||||
'keyschema': {'type': 'string', 'regex': '[a-z]+'}
|
||||
},
|
||||
'a_dict_with_valuesrules': {'type': 'dict', 'valuesrules': {'type': 'integer'}},
|
||||
'a_list_length': {
|
||||
'type': 'list',
|
||||
'schema': {'type': 'integer'},
|
||||
'minlength': 2,
|
||||
'maxlength': 5,
|
||||
},
|
||||
'a_nullable_field_without_type': {
|
||||
'nullable': True
|
||||
},
|
||||
'a_not_nullable_field_without_type': {
|
||||
},
|
||||
'a_nullable_field_without_type': {'nullable': True},
|
||||
'a_not_nullable_field_without_type': {},
|
||||
}
|
||||
|
||||
sample_document = {'name': 'john doe'}
|
||||
|
||||
+49
-14
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from decimal import Decimal
|
||||
from pkg_resources import Distribution, DistributionNotFound
|
||||
|
||||
from pytest import mark
|
||||
|
||||
@@ -8,6 +9,37 @@ from cerberus import TypeDefinition, Validator
|
||||
from cerberus.tests import assert_fail, assert_success
|
||||
from cerberus.utils import validator_factory
|
||||
from cerberus.validator import BareValidator
|
||||
from cerberus.platform import PYTHON_VERSION
|
||||
|
||||
|
||||
if PYTHON_VERSION > 3 and PYTHON_VERSION < 3.4:
|
||||
from imp import reload
|
||||
elif PYTHON_VERSION >= 3.4:
|
||||
from importlib import reload
|
||||
else:
|
||||
pass # Python 2.x
|
||||
|
||||
|
||||
def test_pkgresources_version(monkeypatch):
|
||||
def create_fake_distribution(name):
|
||||
return Distribution(project_name="cerberus", version="1.2.3")
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
cerberus = __import__("cerberus")
|
||||
m.setattr("pkg_resources.get_distribution", create_fake_distribution)
|
||||
reload(cerberus)
|
||||
assert cerberus.__version__ == "1.2.3"
|
||||
|
||||
|
||||
def test_version_not_found(monkeypatch):
|
||||
def raise_distribution_not_found(name):
|
||||
raise DistributionNotFound("pkg_resources cannot get distribution")
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
cerberus = __import__("cerberus")
|
||||
m.setattr("pkg_resources.get_distribution", raise_distribution_not_found)
|
||||
reload(cerberus)
|
||||
assert cerberus.__version__ == "unknown"
|
||||
|
||||
|
||||
def test_clear_cache(validator):
|
||||
@@ -23,8 +55,11 @@ def test_docstring(validator):
|
||||
# Test that testing with the sample schema works as expected
|
||||
# as there might be rules with side-effects in it
|
||||
|
||||
@mark.parametrize('test,document', ((assert_fail, {'an_integer': 60}),
|
||||
(assert_success, {'an_integer': 110})))
|
||||
|
||||
@mark.parametrize(
|
||||
"test,document",
|
||||
((assert_fail, {"an_integer": 60}), (assert_success, {"an_integer": 110})),
|
||||
)
|
||||
def test_that_test_fails(test, document):
|
||||
try:
|
||||
test(document)
|
||||
@@ -35,42 +70,42 @@ def test_that_test_fails(test, document):
|
||||
|
||||
|
||||
def test_dynamic_types():
|
||||
decimal_type = TypeDefinition('decimal', (Decimal,), ())
|
||||
document = {'measurement': Decimal(0)}
|
||||
schema = {'measurement': {'type': 'decimal'}}
|
||||
decimal_type = TypeDefinition("decimal", (Decimal,), ())
|
||||
document = {"measurement": Decimal(0)}
|
||||
schema = {"measurement": {"type": "decimal"}}
|
||||
|
||||
validator = Validator()
|
||||
validator.types_mapping['decimal'] = decimal_type
|
||||
validator.types_mapping["decimal"] = decimal_type
|
||||
assert_success(document, schema, validator)
|
||||
|
||||
class MyValidator(Validator):
|
||||
types_mapping = Validator.types_mapping.copy()
|
||||
types_mapping['decimal'] = decimal_type
|
||||
types_mapping["decimal"] = decimal_type
|
||||
|
||||
validator = MyValidator()
|
||||
assert_success(document, schema, validator)
|
||||
|
||||
|
||||
def test_mro():
|
||||
assert Validator.__mro__ == (Validator, BareValidator, object), \
|
||||
Validator.__mro__
|
||||
assert Validator.__mro__ == (Validator, BareValidator, object), Validator.__mro__
|
||||
|
||||
|
||||
def test_mixin_init():
|
||||
class Mixin(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['test'] = True
|
||||
kwargs["test"] = True
|
||||
super(Mixin, self).__init__(*args, **kwargs)
|
||||
|
||||
MyValidator = validator_factory('MyValidator', Mixin)
|
||||
MyValidator = validator_factory("MyValidator", Mixin)
|
||||
validator = MyValidator()
|
||||
assert validator._config['test']
|
||||
assert validator._config["test"]
|
||||
|
||||
|
||||
def test_sub_init():
|
||||
class MyValidator(Validator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['test'] = True
|
||||
kwargs["test"] = True
|
||||
super(MyValidator, self).__init__(*args, **kwargs)
|
||||
|
||||
validator = MyValidator()
|
||||
assert validator._config['test']
|
||||
assert validator._config["test"]
|
||||
|
||||
+33
-10
@@ -1,12 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pytest import mark
|
||||
|
||||
import cerberus
|
||||
from cerberus.tests import assert_fail, assert_success
|
||||
from cerberus.tests.conftest import sample_schema
|
||||
|
||||
|
||||
def test_contextual_data_preservation():
|
||||
|
||||
class InheritedValidator(cerberus.Validator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'working_dir' in kwargs:
|
||||
@@ -18,9 +19,9 @@ def test_contextual_data_preservation():
|
||||
return True
|
||||
|
||||
assert 'test' in InheritedValidator.types
|
||||
v = InheritedValidator({'test': {'type': 'list',
|
||||
'schema': {'type': 'test'}}},
|
||||
working_dir='/tmp')
|
||||
v = InheritedValidator(
|
||||
{'test': {'type': 'list', 'schema': {'type': 'test'}}}, working_dir='/tmp'
|
||||
)
|
||||
assert_success({'test': ['foo']}, validator=v)
|
||||
|
||||
|
||||
@@ -42,25 +43,47 @@ def test_docstring_parsing():
|
||||
assert 'bar' in CustomValidator.validation_rules
|
||||
|
||||
|
||||
def test_issue_265():
|
||||
# TODO remove 'validator' as rule parameter with the next major release
|
||||
@mark.parametrize('rule', ('check_with', 'validator'))
|
||||
def test_check_with_method(rule):
|
||||
# https://github.com/pyeve/cerberus/issues/265
|
||||
class MyValidator(cerberus.Validator):
|
||||
def _check_with_oddity(self, field, value):
|
||||
if not value & 1:
|
||||
self._error(field, "Must be an odd number")
|
||||
|
||||
v = MyValidator(schema={'amount': {rule: 'oddity'}})
|
||||
assert_success(document={'amount': 1}, validator=v)
|
||||
assert_fail(
|
||||
document={'amount': 2},
|
||||
validator=v,
|
||||
error=('amount', (), cerberus.errors.CUSTOM, None, ('Must be an odd number',)),
|
||||
)
|
||||
|
||||
|
||||
# TODO remove test with the next major release
|
||||
@mark.parametrize('rule', ('check_with', 'validator'))
|
||||
def test_validator_method(rule):
|
||||
class MyValidator(cerberus.Validator):
|
||||
def _validator_oddity(self, field, value):
|
||||
if not value & 1:
|
||||
self._error(field, "Must be an odd number")
|
||||
|
||||
v = MyValidator(schema={'amount': {'validator': 'oddity'}})
|
||||
v = MyValidator(schema={'amount': {rule: 'oddity'}})
|
||||
assert_success(document={'amount': 1}, validator=v)
|
||||
assert_fail(document={'amount': 2}, validator=v,
|
||||
error=('amount', (), cerberus.errors.CUSTOM, None,
|
||||
('Must be an odd number',)))
|
||||
assert_fail(
|
||||
document={'amount': 2},
|
||||
validator=v,
|
||||
error=('amount', (), cerberus.errors.CUSTOM, None, ('Must be an odd number',)),
|
||||
)
|
||||
|
||||
|
||||
def test_schema_validation_can_be_disabled_in_schema_setter():
|
||||
|
||||
class NonvalidatingValidator(cerberus.Validator):
|
||||
"""
|
||||
Skips schema validation to speed up initialization
|
||||
"""
|
||||
|
||||
@cerberus.Validator.schema.setter
|
||||
def schema(self, schema):
|
||||
if schema is None:
|
||||
|
||||
+135
-72
@@ -24,14 +24,14 @@ def test__error_1():
|
||||
|
||||
|
||||
def test__error_2():
|
||||
v = Validator(schema={'foo': {'keyschema': {'type': 'integer'}}})
|
||||
v = Validator(schema={'foo': {'keysrules': {'type': 'integer'}}})
|
||||
v.document = {'foo': {'0': 'bar'}}
|
||||
v._error('foo', errors.KEYSCHEMA, ())
|
||||
v._error('foo', errors.KEYSRULES, ())
|
||||
error = v._errors[0]
|
||||
assert error.document_path == ('foo',)
|
||||
assert error.schema_path == ('foo', 'keyschema')
|
||||
assert error.schema_path == ('foo', 'keysrules')
|
||||
assert error.code == 0x83
|
||||
assert error.rule == 'keyschema'
|
||||
assert error.rule == 'keysrules'
|
||||
assert error.constraint == {'type': 'integer'}
|
||||
assert error.value == {'0': 'bar'}
|
||||
assert error.info == ((),)
|
||||
@@ -40,8 +40,10 @@ def test__error_2():
|
||||
|
||||
|
||||
def test__error_3():
|
||||
valids = [{'type': 'string', 'regex': '0x[0-9a-f]{2}'},
|
||||
{'type': 'integer', 'min': 0, 'max': 255}]
|
||||
valids = [
|
||||
{'type': 'string', 'regex': '0x[0-9a-f]{2}'},
|
||||
{'type': 'integer', 'min': 0, 'max': 255},
|
||||
]
|
||||
v = Validator(schema={'foo': {'oneof': valids}})
|
||||
v.document = {'foo': '0x100'}
|
||||
v._error('foo', errors.ONEOF, (), 0, 2)
|
||||
@@ -77,8 +79,9 @@ def test_error_tree_from_subschema(validator):
|
||||
assert 'bar' in s_error_tree['foo']['schema']
|
||||
assert 'type' in s_error_tree['foo']['schema']['bar']
|
||||
assert s_error_tree['foo']['schema']['bar']['type'].errors[0].value == 0
|
||||
assert s_error_tree.fetch_errors_from(
|
||||
('foo', 'schema', 'bar', 'type'))[0].value == 0
|
||||
assert (
|
||||
s_error_tree.fetch_errors_from(('foo', 'schema', 'bar', 'type'))[0].value == 0
|
||||
)
|
||||
|
||||
|
||||
def test_error_tree_from_anyof(validator):
|
||||
@@ -98,12 +101,17 @@ def test_error_tree_from_anyof(validator):
|
||||
|
||||
|
||||
def test_nested_error_paths(validator):
|
||||
schema = {'a_dict': {'keyschema': {'type': 'integer'},
|
||||
'valueschema': {'regex': '[a-z]*'}},
|
||||
'a_list': {'schema': {'type': 'string',
|
||||
'oneof_regex': ['[a-z]*$', '[A-Z]*']}}}
|
||||
document = {'a_dict': {0: 'abc', 'one': 'abc', 2: 'aBc', 'three': 'abC'},
|
||||
'a_list': [0, 'abc', 'abC']}
|
||||
schema = {
|
||||
'a_dict': {
|
||||
'keysrules': {'type': 'integer'},
|
||||
'valuesrules': {'regex': '[a-z]*'},
|
||||
},
|
||||
'a_list': {'schema': {'type': 'string', 'oneof_regex': ['[a-z]*$', '[A-Z]*']}},
|
||||
}
|
||||
document = {
|
||||
'a_dict': {0: 'abc', 'one': 'abc', 2: 'aBc', 'three': 'abC'},
|
||||
'a_list': [0, 'abc', 'abC'],
|
||||
}
|
||||
assert_fail(document, schema, validator=validator)
|
||||
|
||||
_det = validator.document_error_tree
|
||||
@@ -120,35 +128,59 @@ def test_nested_error_paths(validator):
|
||||
assert len(_det['a_dict'][2].errors) == 1
|
||||
assert len(_det['a_dict']['three'].errors) == 2
|
||||
|
||||
assert len(_set['a_dict']['keyschema'].errors) == 1
|
||||
assert len(_set['a_dict']['valueschema'].errors) == 1
|
||||
assert len(_set['a_dict']['keysrules'].errors) == 1
|
||||
assert len(_set['a_dict']['valuesrules'].errors) == 1
|
||||
|
||||
assert len(_set['a_dict']['keyschema']['type'].errors) == 2
|
||||
assert len(_set['a_dict']['valueschema']['regex'].errors) == 2
|
||||
assert len(_set['a_dict']['keysrules']['type'].errors) == 2
|
||||
assert len(_set['a_dict']['valuesrules']['regex'].errors) == 2
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_dict', 'one'), ('a_dict', 'keyschema', 'type'),
|
||||
errors.BAD_TYPE.code, 'type', 'integer', 'one', ())
|
||||
('a_dict', 'one'),
|
||||
('a_dict', 'keysrules', 'type'),
|
||||
errors.BAD_TYPE.code,
|
||||
'type',
|
||||
'integer',
|
||||
'one',
|
||||
(),
|
||||
)
|
||||
assert _det['a_dict']['one'].errors[0] == _ref_err
|
||||
assert _set['a_dict']['keyschema']['type'].errors[0] == _ref_err
|
||||
assert _set['a_dict']['keysrules']['type'].errors[0] == _ref_err
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_dict', 2), ('a_dict', 'valueschema', 'regex'),
|
||||
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'aBc', ())
|
||||
('a_dict', 2),
|
||||
('a_dict', 'valuesrules', 'regex'),
|
||||
errors.REGEX_MISMATCH.code,
|
||||
'regex',
|
||||
'[a-z]*$',
|
||||
'aBc',
|
||||
(),
|
||||
)
|
||||
assert _det['a_dict'][2].errors[0] == _ref_err
|
||||
assert _set['a_dict']['valueschema']['regex'].errors[0] == _ref_err
|
||||
assert _set['a_dict']['valuesrules']['regex'].errors[0] == _ref_err
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_dict', 'three'), ('a_dict', 'keyschema', 'type'),
|
||||
errors.BAD_TYPE.code, 'type', 'integer', 'three', ())
|
||||
('a_dict', 'three'),
|
||||
('a_dict', 'keysrules', 'type'),
|
||||
errors.BAD_TYPE.code,
|
||||
'type',
|
||||
'integer',
|
||||
'three',
|
||||
(),
|
||||
)
|
||||
assert _det['a_dict']['three'].errors[0] == _ref_err
|
||||
assert _set['a_dict']['keyschema']['type'].errors[1] == _ref_err
|
||||
assert _set['a_dict']['keysrules']['type'].errors[1] == _ref_err
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_dict', 'three'), ('a_dict', 'valueschema', 'regex'),
|
||||
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ())
|
||||
('a_dict', 'three'),
|
||||
('a_dict', 'valuesrules', 'regex'),
|
||||
errors.REGEX_MISMATCH.code,
|
||||
'regex',
|
||||
'[a-z]*$',
|
||||
'abC',
|
||||
(),
|
||||
)
|
||||
assert _det['a_dict']['three'].errors[1] == _ref_err
|
||||
assert _set['a_dict']['valueschema']['regex'].errors[1] == _ref_err
|
||||
assert _set['a_dict']['valuesrules']['regex'].errors[1] == _ref_err
|
||||
|
||||
assert len(_det['a_list'].errors) == 1
|
||||
assert len(_det['a_list'][0].errors) == 1
|
||||
@@ -161,34 +193,56 @@ def test_nested_error_paths(validator):
|
||||
assert len(_set['a_list']['schema']['oneof'][1]['regex'].errors) == 1
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_list', 0), ('a_list', 'schema', 'type'), errors.BAD_TYPE.code,
|
||||
'type', 'string', 0, ())
|
||||
('a_list', 0),
|
||||
('a_list', 'schema', 'type'),
|
||||
errors.BAD_TYPE.code,
|
||||
'type',
|
||||
'string',
|
||||
0,
|
||||
(),
|
||||
)
|
||||
assert _det['a_list'][0].errors[0] == _ref_err
|
||||
assert _set['a_list']['schema']['type'].errors[0] == _ref_err
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_list', 2), ('a_list', 'schema', 'oneof'), errors.ONEOF.code,
|
||||
'oneof', 'irrelevant_at_this_point', 'abC', ())
|
||||
('a_list', 2),
|
||||
('a_list', 'schema', 'oneof'),
|
||||
errors.ONEOF.code,
|
||||
'oneof',
|
||||
'irrelevant_at_this_point',
|
||||
'abC',
|
||||
(),
|
||||
)
|
||||
assert _det['a_list'][2].errors[0] == _ref_err
|
||||
assert _set['a_list']['schema']['oneof'].errors[0] == _ref_err
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_list', 2), ('a_list', 'schema', 'oneof', 0, 'regex'),
|
||||
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ())
|
||||
('a_list', 2),
|
||||
('a_list', 'schema', 'oneof', 0, 'regex'),
|
||||
errors.REGEX_MISMATCH.code,
|
||||
'regex',
|
||||
'[a-z]*$',
|
||||
'abC',
|
||||
(),
|
||||
)
|
||||
assert _det['a_list'][2].errors[1] == _ref_err
|
||||
assert _set['a_list']['schema']['oneof'][0]['regex'].errors[0] == _ref_err
|
||||
|
||||
_ref_err = ValidationError(
|
||||
('a_list', 2), ('a_list', 'schema', 'oneof', 1, 'regex'),
|
||||
errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ())
|
||||
('a_list', 2),
|
||||
('a_list', 'schema', 'oneof', 1, 'regex'),
|
||||
errors.REGEX_MISMATCH.code,
|
||||
'regex',
|
||||
'[a-z]*$',
|
||||
'abC',
|
||||
(),
|
||||
)
|
||||
assert _det['a_list'][2].errors[2] == _ref_err
|
||||
assert _set['a_list']['schema']['oneof'][1]['regex'].errors[0] == _ref_err
|
||||
|
||||
|
||||
def test_queries():
|
||||
schema = {'foo': {'type': 'dict',
|
||||
'schema':
|
||||
{'bar': {'type': 'number'}}}}
|
||||
schema = {'foo': {'type': 'dict', 'schema': {'bar': {'type': 'number'}}}}
|
||||
document = {'foo': {'bar': 'zero'}}
|
||||
validator = Validator(schema)
|
||||
validator(document)
|
||||
@@ -202,59 +256,68 @@ def test_queries():
|
||||
assert errors.MAPPING_SCHEMA in validator.document_error_tree['foo']
|
||||
assert errors.BAD_TYPE in validator.document_error_tree['foo']['bar']
|
||||
assert errors.MAPPING_SCHEMA in validator.schema_error_tree['foo']['schema']
|
||||
assert errors.BAD_TYPE in \
|
||||
validator.schema_error_tree['foo']['schema']['bar']['type']
|
||||
assert (
|
||||
errors.BAD_TYPE in validator.schema_error_tree['foo']['schema']['bar']['type']
|
||||
)
|
||||
|
||||
assert (validator.document_error_tree['foo'][errors.MAPPING_SCHEMA]
|
||||
.child_errors[0].code == errors.BAD_TYPE.code)
|
||||
assert (
|
||||
validator.document_error_tree['foo'][errors.MAPPING_SCHEMA].child_errors[0].code
|
||||
== errors.BAD_TYPE.code
|
||||
)
|
||||
|
||||
|
||||
def test_basic_error_handler():
|
||||
handler = errors.BasicErrorHandler()
|
||||
_errors, ref = [], {}
|
||||
|
||||
_errors.append(ValidationError(
|
||||
['foo'], ['foo'], 0x63, 'readonly', True, None, ()))
|
||||
_errors.append(ValidationError(['foo'], ['foo'], 0x63, 'readonly', True, None, ()))
|
||||
ref.update({'foo': [handler.messages[0x63]]})
|
||||
assert handler(_errors) == ref
|
||||
|
||||
_errors.append(ValidationError(
|
||||
['bar'], ['foo'], 0x42, 'min', 1, 2, ()))
|
||||
_errors.append(ValidationError(['bar'], ['foo'], 0x42, 'min', 1, 2, ()))
|
||||
ref.update({'bar': [handler.messages[0x42].format(constraint=1)]})
|
||||
assert handler(_errors) == ref
|
||||
|
||||
_errors.append(ValidationError(
|
||||
['zap', 'foo'], ['zap', 'schema', 'foo'], 0x24, 'type', 'string',
|
||||
True, ()))
|
||||
ref.update({'zap': [{'foo': [handler.messages[0x24].format(
|
||||
constraint='string')]}]})
|
||||
_errors.append(
|
||||
ValidationError(
|
||||
['zap', 'foo'], ['zap', 'schema', 'foo'], 0x24, 'type', 'string', True, ()
|
||||
)
|
||||
)
|
||||
ref.update({'zap': [{'foo': [handler.messages[0x24].format(constraint='string')]}]})
|
||||
assert handler(_errors) == ref
|
||||
|
||||
_errors.append(ValidationError(
|
||||
['zap', 'foo'], ['zap', 'schema', 'foo'], 0x41, 'regex',
|
||||
'^p[äe]ng$', 'boom', ()))
|
||||
ref['zap'][0]['foo'].append(
|
||||
handler.messages[0x41].format(constraint='^p[äe]ng$'))
|
||||
_errors.append(
|
||||
ValidationError(
|
||||
['zap', 'foo'],
|
||||
['zap', 'schema', 'foo'],
|
||||
0x41,
|
||||
'regex',
|
||||
'^p[äe]ng$',
|
||||
'boom',
|
||||
(),
|
||||
)
|
||||
)
|
||||
ref['zap'][0]['foo'].append(handler.messages[0x41].format(constraint='^p[äe]ng$'))
|
||||
assert handler(_errors) == ref
|
||||
|
||||
|
||||
def test_basic_error_of_errors(validator):
|
||||
schema = {'foo': {'oneof': [
|
||||
{'type': 'integer'},
|
||||
{'type': 'string'}
|
||||
]}}
|
||||
schema = {'foo': {'oneof': [{'type': 'integer'}, {'type': 'string'}]}}
|
||||
document = {'foo': 23.42}
|
||||
error = ('foo', ('foo', 'oneof'), errors.ONEOF,
|
||||
schema['foo']['oneof'], ())
|
||||
error = ('foo', ('foo', 'oneof'), errors.ONEOF, schema['foo']['oneof'], ())
|
||||
child_errors = [
|
||||
(error[0], error[1] + (0, 'type'), errors.BAD_TYPE, 'integer'),
|
||||
(error[0], error[1] + (1, 'type'), errors.BAD_TYPE, 'string')
|
||||
(error[0], error[1] + (1, 'type'), errors.BAD_TYPE, 'string'),
|
||||
]
|
||||
assert_fail(document, schema, validator=validator,
|
||||
error=error, child_errors=child_errors)
|
||||
assert_fail(
|
||||
document, schema, validator=validator, error=error, child_errors=child_errors
|
||||
)
|
||||
assert validator.errors == {
|
||||
'foo': [errors.BasicErrorHandler.messages[0x92],
|
||||
{'oneof definition 0': ['must be of integer type'],
|
||||
'oneof definition 1': ['must be of string type']}
|
||||
]
|
||||
'foo': [
|
||||
errors.BasicErrorHandler.messages[0x92],
|
||||
{
|
||||
'oneof definition 0': ['must be of integer type'],
|
||||
'oneof definition 1': ['must be of string type'],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
+213
-155
@@ -1,10 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from copy import deepcopy
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from pytest import mark
|
||||
|
||||
from cerberus import Validator, errors
|
||||
from cerberus.tests import (assert_fail, assert_has_error, assert_normalized,
|
||||
assert_success)
|
||||
from cerberus.tests import (
|
||||
assert_fail,
|
||||
assert_has_error,
|
||||
assert_normalized,
|
||||
assert_success,
|
||||
)
|
||||
|
||||
|
||||
def must_not_be_called(*args, **kwargs):
|
||||
raise RuntimeError('This shall not be called.')
|
||||
|
||||
|
||||
def test_coerce():
|
||||
@@ -15,21 +26,31 @@ def test_coerce():
|
||||
|
||||
|
||||
def test_coerce_in_dictschema():
|
||||
schema = {'thing': {'type': 'dict',
|
||||
'schema': {'amount': {'coerce': int}}}}
|
||||
schema = {'thing': {'type': 'dict', 'schema': {'amount': {'coerce': int}}}}
|
||||
document = {'thing': {'amount': '2'}}
|
||||
expected = {'thing': {'amount': 2}}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
|
||||
def test_coerce_in_listschema():
|
||||
schema = {'things': {'type': 'list',
|
||||
'schema': {'coerce': int}}}
|
||||
schema = {'things': {'type': 'list', 'schema': {'coerce': int}}}
|
||||
document = {'things': ['1', '2', '3']}
|
||||
expected = {'things': [1, 2, 3]}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
|
||||
def test_coerce_in_listitems():
|
||||
schema = {'things': {'type': 'list', 'items': [{'coerce': int}, {'coerce': str}]}}
|
||||
document = {'things': ['1', 2]}
|
||||
expected = {'things': [1, '2']}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
validator = Validator(schema)
|
||||
document['things'].append(3)
|
||||
assert not validator(document)
|
||||
assert validator.document['things'] == document['things']
|
||||
|
||||
|
||||
def test_coerce_in_dictschema_in_listschema():
|
||||
item_schema = {'type': 'dict', 'schema': {'amount': {'coerce': int}}}
|
||||
schema = {'things': {'type': 'list', 'schema': item_schema}}
|
||||
@@ -39,9 +60,7 @@ def test_coerce_in_dictschema_in_listschema():
|
||||
|
||||
|
||||
def test_coerce_not_destructive():
|
||||
schema = {
|
||||
'amount': {'coerce': int}
|
||||
}
|
||||
schema = {'amount': {'coerce': int}}
|
||||
v = Validator(schema)
|
||||
doc = {'amount': '1'}
|
||||
v.validate(doc)
|
||||
@@ -52,16 +71,48 @@ def test_coerce_catches_ValueError():
|
||||
schema = {'amount': {'coerce': int}}
|
||||
_errors = assert_fail({'amount': 'not_a_number'}, schema)
|
||||
_errors[0].info = () # ignore exception message here
|
||||
assert_has_error(_errors, 'amount', ('amount', 'coerce'),
|
||||
errors.COERCION_FAILED, int)
|
||||
assert_has_error(
|
||||
_errors, 'amount', ('amount', 'coerce'), errors.COERCION_FAILED, int
|
||||
)
|
||||
|
||||
|
||||
def test_coerce_in_listitems_catches_ValueError():
|
||||
schema = {'things': {'type': 'list', 'items': [{'coerce': int}, {'coerce': str}]}}
|
||||
document = {'things': ['not_a_number', 2]}
|
||||
_errors = assert_fail(document, schema)
|
||||
_errors[0].info = () # ignore exception message here
|
||||
assert_has_error(
|
||||
_errors,
|
||||
('things', 0),
|
||||
('things', 'items', 'coerce'),
|
||||
errors.COERCION_FAILED,
|
||||
int,
|
||||
)
|
||||
|
||||
|
||||
def test_coerce_catches_TypeError():
|
||||
schema = {'name': {'coerce': str.lower}}
|
||||
_errors = assert_fail({'name': 1234}, schema)
|
||||
_errors[0].info = () # ignore exception message here
|
||||
assert_has_error(_errors, 'name', ('name', 'coerce'),
|
||||
errors.COERCION_FAILED, str.lower)
|
||||
assert_has_error(
|
||||
_errors, 'name', ('name', 'coerce'), errors.COERCION_FAILED, str.lower
|
||||
)
|
||||
|
||||
|
||||
def test_coerce_in_listitems_catches_TypeError():
|
||||
schema = {
|
||||
'things': {'type': 'list', 'items': [{'coerce': int}, {'coerce': str.lower}]}
|
||||
}
|
||||
document = {'things': ['1', 2]}
|
||||
_errors = assert_fail(document, schema)
|
||||
_errors[0].info = () # ignore exception message here
|
||||
assert_has_error(
|
||||
_errors,
|
||||
('things', 1),
|
||||
('things', 'items', 'coerce'),
|
||||
errors.COERCION_FAILED,
|
||||
str.lower,
|
||||
)
|
||||
|
||||
|
||||
def test_coerce_unknown():
|
||||
@@ -88,16 +139,16 @@ def test_custom_coerce_and_rename():
|
||||
|
||||
|
||||
def test_coerce_chain():
|
||||
drop_prefix = lambda x: x[2:]
|
||||
upper = lambda x: x.upper()
|
||||
drop_prefix = lambda x: x[2:] # noqa: E731
|
||||
upper = lambda x: x.upper() # noqa: E731
|
||||
schema = {'foo': {'coerce': [hex, drop_prefix, upper]}}
|
||||
assert_normalized({'foo': 15}, {'foo': 'F'}, schema)
|
||||
|
||||
|
||||
def test_coerce_chain_aborts(validator):
|
||||
def dont_do_me(value):
|
||||
raise AssertionError('The coercion chain did not abort after an '
|
||||
'error.')
|
||||
raise AssertionError('The coercion chain did not abort after an ' 'error.')
|
||||
|
||||
schema = {'foo': {'coerce': [hex, dont_do_me]}}
|
||||
validator({'foo': '0'}, schema)
|
||||
assert errors.COERCION_FAILED in validator._errors
|
||||
@@ -105,12 +156,12 @@ def test_coerce_chain_aborts(validator):
|
||||
|
||||
def test_coerce_non_digit_in_sequence(validator):
|
||||
# https://github.com/pyeve/cerberus/issues/211
|
||||
schema = {'data': {'type': 'list',
|
||||
'schema': {'type': 'integer', 'coerce': int}}}
|
||||
schema = {'data': {'type': 'list', 'schema': {'type': 'integer', 'coerce': int}}}
|
||||
document = {'data': ['q']}
|
||||
assert validator.validated(document, schema) is None
|
||||
assert (validator.validated(document, schema, always_return_document=True)
|
||||
== document) # noqa: W503
|
||||
assert (
|
||||
validator.validated(document, schema, always_return_document=True) == document
|
||||
) # noqa: W503
|
||||
|
||||
|
||||
def test_nullables_dont_fail_coerce():
|
||||
@@ -119,6 +170,18 @@ def test_nullables_dont_fail_coerce():
|
||||
assert_normalized(document, document, schema)
|
||||
|
||||
|
||||
def test_nullables_fail_coerce_on_non_null_values(validator):
|
||||
def failing_coercion(value):
|
||||
raise Exception("expected to fail")
|
||||
|
||||
schema = {'foo': {'coerce': failing_coercion, 'nullable': True, 'type': 'integer'}}
|
||||
document = {'foo': None}
|
||||
assert_normalized(document, document, schema)
|
||||
|
||||
validator({'foo': 2}, schema)
|
||||
assert errors.COERCION_FAILED in validator._errors
|
||||
|
||||
|
||||
def test_normalized():
|
||||
schema = {'amount': {'coerce': int}}
|
||||
document = {'amount': '2'}
|
||||
@@ -154,9 +217,13 @@ def test_purge_unknown():
|
||||
|
||||
|
||||
def test_purge_unknown_in_subschema():
|
||||
schema = {'foo': {'type': 'dict',
|
||||
'schema': {'foo': {'type': 'string'}},
|
||||
'purge_unknown': True}}
|
||||
schema = {
|
||||
'foo': {
|
||||
'type': 'dict',
|
||||
'schema': {'foo': {'type': 'string'}},
|
||||
'purge_unknown': True,
|
||||
}
|
||||
}
|
||||
document = {'foo': {'bar': ''}}
|
||||
expected = {'foo': {}}
|
||||
assert_normalized(document, expected, schema)
|
||||
@@ -175,8 +242,7 @@ def test_issue_147_complex():
|
||||
|
||||
|
||||
def test_issue_147_nested_dict():
|
||||
schema = {'thing': {'type': 'dict',
|
||||
'schema': {'amount': {'coerce': int}}}}
|
||||
schema = {'thing': {'type': 'dict', 'schema': {'amount': {'coerce': int}}}}
|
||||
ref_obj = '2'
|
||||
document = {'thing': {'amount': ref_obj}}
|
||||
normalized = Validator(schema).normalized(document)
|
||||
@@ -186,20 +252,21 @@ def test_issue_147_nested_dict():
|
||||
assert document['thing']['amount'] is ref_obj
|
||||
|
||||
|
||||
def test_coerce_in_valueschema():
|
||||
def test_coerce_in_valuesrules():
|
||||
# https://github.com/pyeve/cerberus/issues/155
|
||||
schema = {'thing': {'type': 'dict',
|
||||
'valueschema': {'coerce': int,
|
||||
'type': 'integer'}}}
|
||||
schema = {
|
||||
'thing': {'type': 'dict', 'valuesrules': {'coerce': int, 'type': 'integer'}}
|
||||
}
|
||||
document = {'thing': {'amount': '2'}}
|
||||
expected = {'thing': {'amount': 2}}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
|
||||
def test_coerce_in_keyschema():
|
||||
def test_coerce_in_keysrules():
|
||||
# https://github.com/pyeve/cerberus/issues/155
|
||||
schema = {'thing': {'type': 'dict',
|
||||
'keyschema': {'coerce': int, 'type': 'integer'}}}
|
||||
schema = {
|
||||
'thing': {'type': 'dict', 'keysrules': {'coerce': int, 'type': 'integer'}}
|
||||
}
|
||||
document = {'thing': {'5': 'foo'}}
|
||||
expected = {'thing': {5: 'foo'}}
|
||||
assert_normalized(document, expected, schema)
|
||||
@@ -207,8 +274,7 @@ def test_coerce_in_keyschema():
|
||||
|
||||
def test_coercion_of_sequence_items(validator):
|
||||
# https://github.com/pyeve/cerberus/issues/161
|
||||
schema = {'a_list': {'type': 'list', 'schema': {'type': 'float',
|
||||
'coerce': float}}}
|
||||
schema = {'a_list': {'type': 'list', 'schema': {'type': 'float', 'coerce': float}}}
|
||||
document = {'a_list': [3, 4, 5]}
|
||||
expected = {'a_list': [3.0, 4.0, 5.0]}
|
||||
assert_normalized(document, expected, schema, validator)
|
||||
@@ -216,110 +282,76 @@ def test_coercion_of_sequence_items(validator):
|
||||
assert isinstance(x, float)
|
||||
|
||||
|
||||
def test_default_missing():
|
||||
_test_default_missing({'default': 'bar_value'})
|
||||
|
||||
|
||||
def test_default_setter_missing():
|
||||
_test_default_missing({'default_setter': lambda doc: 'bar_value'})
|
||||
|
||||
|
||||
def _test_default_missing(default):
|
||||
@mark.parametrize(
|
||||
'default', ({'default': 'bar_value'}, {'default_setter': lambda doc: 'bar_value'})
|
||||
)
|
||||
def test_default_missing(default):
|
||||
bar_schema = {'type': 'string'}
|
||||
bar_schema.update(default)
|
||||
schema = {'foo': {'type': 'string'},
|
||||
'bar': bar_schema}
|
||||
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
|
||||
document = {'foo': 'foo_value'}
|
||||
expected = {'foo': 'foo_value', 'bar': 'bar_value'}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
|
||||
def test_default_existent():
|
||||
_test_default_existent({'default': 'bar_value'})
|
||||
|
||||
|
||||
def test_default_setter_existent():
|
||||
def raise_error(doc):
|
||||
raise RuntimeError('should not be called')
|
||||
_test_default_existent({'default_setter': raise_error})
|
||||
|
||||
|
||||
def _test_default_existent(default):
|
||||
@mark.parametrize(
|
||||
'default', ({'default': 'bar_value'}, {'default_setter': must_not_be_called})
|
||||
)
|
||||
def test_default_existent(default):
|
||||
bar_schema = {'type': 'string'}
|
||||
bar_schema.update(default)
|
||||
schema = {'foo': {'type': 'string'},
|
||||
'bar': bar_schema}
|
||||
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
|
||||
document = {'foo': 'foo_value', 'bar': 'non_default'}
|
||||
assert_normalized(document, document.copy(), schema)
|
||||
|
||||
|
||||
def test_default_none_nullable():
|
||||
_test_default_none_nullable({'default': 'bar_value'})
|
||||
|
||||
|
||||
def test_default_setter_none_nullable():
|
||||
def raise_error(doc):
|
||||
raise RuntimeError('should not be called')
|
||||
_test_default_none_nullable({'default_setter': raise_error})
|
||||
|
||||
|
||||
def _test_default_none_nullable(default):
|
||||
bar_schema = {'type': 'string',
|
||||
'nullable': True}
|
||||
@mark.parametrize(
|
||||
'default', ({'default': 'bar_value'}, {'default_setter': must_not_be_called})
|
||||
)
|
||||
def test_default_none_nullable(default):
|
||||
bar_schema = {'type': 'string', 'nullable': True}
|
||||
bar_schema.update(default)
|
||||
schema = {'foo': {'type': 'string'},
|
||||
'bar': bar_schema}
|
||||
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
|
||||
document = {'foo': 'foo_value', 'bar': None}
|
||||
assert_normalized(document, document.copy(), schema)
|
||||
|
||||
|
||||
def test_default_none_nonnullable():
|
||||
_test_default_none_nullable({'default': 'bar_value'})
|
||||
|
||||
|
||||
def test_default_setter_none_nonnullable():
|
||||
_test_default_none_nullable(
|
||||
{'default_setter': lambda doc: 'bar_value'})
|
||||
|
||||
|
||||
def _test_default_none_nonnullable(default):
|
||||
bar_schema = {'type': 'string',
|
||||
'nullable': False}
|
||||
@mark.parametrize(
|
||||
'default', ({'default': 'bar_value'}, {'default_setter': lambda doc: 'bar_value'})
|
||||
)
|
||||
def test_default_none_nonnullable(default):
|
||||
bar_schema = {'type': 'string', 'nullable': False}
|
||||
bar_schema.update(default)
|
||||
schema = {'foo': {'type': 'string'},
|
||||
'bar': bar_schema}
|
||||
document = {'foo': 'foo_value', 'bar': 'bar_value'}
|
||||
assert_normalized(document, document.copy(), schema)
|
||||
schema = {'foo': {'type': 'string'}, 'bar': bar_schema}
|
||||
document = {'foo': 'foo_value', 'bar': None}
|
||||
expected = {'foo': 'foo_value', 'bar': 'bar_value'}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
|
||||
def test_default_none_default_value():
|
||||
schema = {'foo': {'type': 'string'},
|
||||
'bar': {'type': 'string',
|
||||
'nullable': True,
|
||||
'default': None}}
|
||||
schema = {
|
||||
'foo': {'type': 'string'},
|
||||
'bar': {'type': 'string', 'nullable': True, 'default': None},
|
||||
}
|
||||
document = {'foo': 'foo_value'}
|
||||
expected = {'foo': 'foo_value', 'bar': None}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
|
||||
def test_default_missing_in_subschema():
|
||||
_test_default_missing_in_subschema({'default': 'bar_value'})
|
||||
|
||||
|
||||
def test_default_setter_missing_in_subschema():
|
||||
_test_default_missing_in_subschema(
|
||||
{'default_setter': lambda doc: 'bar_value'})
|
||||
|
||||
|
||||
def _test_default_missing_in_subschema(default):
|
||||
@mark.parametrize(
|
||||
'default', ({'default': 'bar_value'}, {'default_setter': lambda doc: 'bar_value'})
|
||||
)
|
||||
def test_default_missing_in_subschema(default):
|
||||
bar_schema = {'type': 'string'}
|
||||
bar_schema.update(default)
|
||||
schema = {'thing': {'type': 'dict',
|
||||
'schema': {'foo': {'type': 'string'},
|
||||
'bar': bar_schema}}}
|
||||
schema = {
|
||||
'thing': {
|
||||
'type': 'dict',
|
||||
'schema': {'foo': {'type': 'string'}, 'bar': bar_schema},
|
||||
}
|
||||
}
|
||||
document = {'thing': {'foo': 'foo_value'}}
|
||||
expected = {'thing': {'foo': 'foo_value',
|
||||
'bar': 'bar_value'}}
|
||||
expected = {'thing': {'foo': 'foo_value', 'bar': 'bar_value'}}
|
||||
assert_normalized(document, expected, schema)
|
||||
|
||||
|
||||
@@ -328,8 +360,7 @@ def test_depending_default_setters():
|
||||
'a': {'type': 'integer'},
|
||||
'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1},
|
||||
'c': {'type': 'integer', 'default_setter': lambda d: d['b'] * 2},
|
||||
'd': {'type': 'integer',
|
||||
'default_setter': lambda d: d['b'] + d['c']}
|
||||
'd': {'type': 'integer', 'default_setter': lambda d: d['b'] + d['c']},
|
||||
}
|
||||
document = {'a': 1}
|
||||
expected = {'a': 1, 'b': 2, 'c': 4, 'd': 6}
|
||||
@@ -339,7 +370,7 @@ def test_depending_default_setters():
|
||||
def test_circular_depending_default_setters(validator):
|
||||
schema = {
|
||||
'a': {'type': 'integer', 'default_setter': lambda d: d['b'] + 1},
|
||||
'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1}
|
||||
'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1},
|
||||
}
|
||||
validator({}, schema)
|
||||
assert errors.SETTING_DEFAULT_FAILED in validator._errors
|
||||
@@ -353,14 +384,16 @@ def test_issue_250():
|
||||
'schema': {
|
||||
'type': 'dict',
|
||||
'allow_unknown': True,
|
||||
'schema': {'a': {'type': 'string'}}
|
||||
}
|
||||
'schema': {'a': {'type': 'string'}},
|
||||
},
|
||||
}
|
||||
}
|
||||
document = {'list': {'is_a': 'mapping'}}
|
||||
assert_fail(document, schema,
|
||||
error=('list', ('list', 'type'), errors.BAD_TYPE,
|
||||
schema['list']['type']))
|
||||
assert_fail(
|
||||
document,
|
||||
schema,
|
||||
error=('list', ('list', 'type'), errors.BAD_TYPE, schema['list']['type']),
|
||||
)
|
||||
|
||||
|
||||
def test_issue_250_no_type_pass_on_list():
|
||||
@@ -370,7 +403,7 @@ def test_issue_250_no_type_pass_on_list():
|
||||
'schema': {
|
||||
'allow_unknown': True,
|
||||
'type': 'dict',
|
||||
'schema': {'a': {'type': 'string'}}
|
||||
'schema': {'a': {'type': 'string'}},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,28 +414,25 @@ def test_issue_250_no_type_pass_on_list():
|
||||
def test_issue_250_no_type_fail_on_dict():
|
||||
# https://github.com/pyeve/cerberus/issues/250
|
||||
schema = {
|
||||
'list': {
|
||||
'schema': {
|
||||
'allow_unknown': True,
|
||||
'schema': {'a': {'type': 'string'}}
|
||||
}
|
||||
}
|
||||
'list': {'schema': {'allow_unknown': True, 'schema': {'a': {'type': 'string'}}}}
|
||||
}
|
||||
document = {'list': {'a': {'a': 'known'}}}
|
||||
assert_fail(document, schema,
|
||||
error=('list', ('list', 'schema'), errors.BAD_TYPE_FOR_SCHEMA,
|
||||
schema['list']['schema']))
|
||||
assert_fail(
|
||||
document,
|
||||
schema,
|
||||
error=(
|
||||
'list',
|
||||
('list', 'schema'),
|
||||
errors.BAD_TYPE_FOR_SCHEMA,
|
||||
schema['list']['schema'],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_issue_250_no_type_fail_pass_on_other():
|
||||
# https://github.com/pyeve/cerberus/issues/250
|
||||
schema = {
|
||||
'list': {
|
||||
'schema': {
|
||||
'allow_unknown': True,
|
||||
'schema': {'a': {'type': 'string'}}
|
||||
}
|
||||
}
|
||||
'list': {'schema': {'allow_unknown': True, 'schema': {'a': {'type': 'string'}}}}
|
||||
}
|
||||
document = {'list': 1}
|
||||
assert_normalized(document, document, schema)
|
||||
@@ -416,21 +446,20 @@ def test_allow_unknown_with_of_rules():
|
||||
{
|
||||
'type': 'dict',
|
||||
'allow_unknown': True,
|
||||
'schema': {'known': {'type': 'string'}}
|
||||
},
|
||||
{
|
||||
'type': 'dict',
|
||||
'schema': {'known': {'type': 'string'}}
|
||||
'schema': {'known': {'type': 'string'}},
|
||||
},
|
||||
{'type': 'dict', 'schema': {'known': {'type': 'string'}}},
|
||||
]
|
||||
}
|
||||
}
|
||||
# check regression and that allow unknown does not cause any different
|
||||
# than expected behaviour for one-of.
|
||||
document = {'test': {'known': 's'}}
|
||||
assert_fail(document, schema,
|
||||
error=('test', ('test', 'oneof'),
|
||||
errors.ONEOF, schema['test']['oneof']))
|
||||
assert_fail(
|
||||
document,
|
||||
schema,
|
||||
error=('test', ('test', 'oneof'), errors.ONEOF, schema['test']['oneof']),
|
||||
)
|
||||
# check that allow_unknown is actually applied
|
||||
document = {'test': {'known': 's', 'unknown': 'asd'}}
|
||||
assert_success(document, schema)
|
||||
@@ -439,18 +468,20 @@ def test_allow_unknown_with_of_rules():
|
||||
def test_271_normalising_tuples():
|
||||
# https://github.com/pyeve/cerberus/issues/271
|
||||
schema = {
|
||||
'my_field': {
|
||||
'type': 'list',
|
||||
'schema': {'type': ('string', 'number', 'dict')}
|
||||
}
|
||||
'my_field': {'type': 'list', 'schema': {'type': ('string', 'number', 'dict')}}
|
||||
}
|
||||
document = {'my_field': ('foo', 'bar', 42, 'albert',
|
||||
'kandinsky', {'items': 23})}
|
||||
document = {'my_field': ('foo', 'bar', 42, 'albert', 'kandinsky', {'items': 23})}
|
||||
assert_success(document, schema)
|
||||
|
||||
normalized = Validator(schema).normalized(document)
|
||||
assert normalized['my_field'] == ('foo', 'bar', 42, 'albert',
|
||||
'kandinsky', {'items': 23})
|
||||
assert normalized['my_field'] == (
|
||||
'foo',
|
||||
'bar',
|
||||
42,
|
||||
'albert',
|
||||
'kandinsky',
|
||||
{'items': 23},
|
||||
)
|
||||
|
||||
|
||||
def test_allow_unknown_wo_schema():
|
||||
@@ -472,14 +503,41 @@ def test_allow_unknown_with_purge_unknown_subdocument():
|
||||
schema = {
|
||||
'foo': {
|
||||
'type': 'dict',
|
||||
'schema': {
|
||||
'bar': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'allow_unknown': True
|
||||
'schema': {'bar': {'type': 'string'}},
|
||||
'allow_unknown': True,
|
||||
}
|
||||
}
|
||||
document = {'foo': {'bar': 'baz', 'corge': False}, 'thud': 'xyzzy'}
|
||||
expected = {'foo': {'bar': 'baz', 'corge': False}}
|
||||
assert_normalized(document, expected, schema, validator)
|
||||
|
||||
|
||||
def test_purge_readonly():
|
||||
schema = {
|
||||
'description': {'type': 'string', 'maxlength': 500},
|
||||
'last_updated': {'readonly': True},
|
||||
}
|
||||
validator = Validator(schema=schema, purge_readonly=True)
|
||||
document = {'description': 'it is a thing'}
|
||||
expected = deepcopy(document)
|
||||
document['last_updated'] = 'future'
|
||||
assert_normalized(document, expected, validator=validator)
|
||||
|
||||
|
||||
def test_defaults_in_allow_unknown_schema():
|
||||
schema = {'meta': {'type': 'dict'}, 'version': {'type': 'string'}}
|
||||
allow_unknown = {
|
||||
'type': 'dict',
|
||||
'schema': {
|
||||
'cfg_path': {'type': 'string', 'default': 'cfg.yaml'},
|
||||
'package': {'type': 'string'},
|
||||
},
|
||||
}
|
||||
validator = Validator(schema=schema, allow_unknown=allow_unknown)
|
||||
|
||||
document = {'version': '1.2.3', 'plugin_foo': {'package': 'foo'}}
|
||||
expected = {
|
||||
'version': '1.2.3',
|
||||
'plugin_foo': {'package': 'foo', 'cfg_path': 'cfg.yaml'},
|
||||
}
|
||||
assert_normalized(document, expected, schema, validator)
|
||||
|
||||
+13
-11
@@ -1,14 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from cerberus import schema_registry, rules_set_registry, Validator
|
||||
from cerberus.tests import (assert_fail, assert_normalized,
|
||||
assert_schema_error, assert_success)
|
||||
from cerberus.tests import (
|
||||
assert_fail,
|
||||
assert_normalized,
|
||||
assert_schema_error,
|
||||
assert_success,
|
||||
)
|
||||
|
||||
|
||||
def test_schema_registry_simple():
|
||||
schema_registry.add('foo', {'bar': {'type': 'string'}})
|
||||
schema = {'a': {'schema': 'foo'},
|
||||
'b': {'schema': 'foo'}}
|
||||
schema = {'a': {'schema': 'foo'}, 'b': {'schema': 'foo'}}
|
||||
document = {'a': {'bar': 'a'}, 'b': {'bar': 'b'}}
|
||||
assert_success(document, schema)
|
||||
|
||||
@@ -33,23 +36,22 @@ def test_allow_unknown_as_reference():
|
||||
|
||||
|
||||
def test_recursion():
|
||||
rules_set_registry.add('self',
|
||||
{'type': 'dict', 'allow_unknown': 'self'})
|
||||
rules_set_registry.add('self', {'type': 'dict', 'allow_unknown': 'self'})
|
||||
v = Validator(allow_unknown='self')
|
||||
assert_success({0: {1: {2: {}}}}, {}, v)
|
||||
|
||||
|
||||
def test_references_remain_unresolved(validator):
|
||||
rules_set_registry.extend((('boolean', {'type': 'boolean'}),
|
||||
('booleans', {'valueschema': 'boolean'})))
|
||||
rules_set_registry.extend(
|
||||
(('boolean', {'type': 'boolean'}), ('booleans', {'valuesrules': 'boolean'}))
|
||||
)
|
||||
validator.schema = {'foo': 'booleans'}
|
||||
assert 'booleans' == validator.schema['foo']
|
||||
assert 'boolean' == rules_set_registry._storage['booleans']['valueschema']
|
||||
assert 'boolean' == rules_set_registry._storage['booleans']['valuesrules']
|
||||
|
||||
|
||||
def test_rules_registry_with_anyof_type():
|
||||
rules_set_registry.add('string_or_integer',
|
||||
{'anyof_type': ['string', 'integer']})
|
||||
rules_set_registry.add('string_or_integer', {'anyof_type': ['string', 'integer']})
|
||||
schema = {'soi': 'string_or_integer'}
|
||||
assert_success({'soi': 'hello'}, schema)
|
||||
|
||||
|
||||
+90
-27
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from cerberus import Validator, errors, SchemaError
|
||||
@@ -9,14 +11,14 @@ from cerberus.tests import assert_schema_error
|
||||
|
||||
def test_empty_schema():
|
||||
validator = Validator()
|
||||
with pytest.raises(SchemaError, message=errors.SCHEMA_ERROR_MISSING):
|
||||
with pytest.raises(SchemaError, match=errors.SCHEMA_ERROR_MISSING):
|
||||
validator({}, schema=None)
|
||||
|
||||
|
||||
def test_bad_schema_type(validator):
|
||||
schema = "this string should really be dict"
|
||||
exp_msg = errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema)
|
||||
with pytest.raises(SchemaError, message=exp_msg):
|
||||
msg = errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema)
|
||||
with pytest.raises(SchemaError, match=msg):
|
||||
validator.schema = schema
|
||||
|
||||
|
||||
@@ -28,23 +30,21 @@ def test_bad_schema_type_field(validator):
|
||||
|
||||
|
||||
def test_unknown_rule(validator):
|
||||
message = "{'foo': [{'unknown': ['unknown rule']}]}"
|
||||
with pytest.raises(SchemaError, message=message):
|
||||
msg = "{'foo': [{'unknown': ['unknown rule']}]}"
|
||||
with pytest.raises(SchemaError, match=re.escape(msg)):
|
||||
validator.schema = {'foo': {'unknown': 'rule'}}
|
||||
|
||||
|
||||
def test_unknown_type(validator):
|
||||
field = 'name'
|
||||
value = 'catch_me'
|
||||
message = str({field: [{'type': ['unallowed value %s' % value]}]})
|
||||
with pytest.raises(SchemaError, message=message):
|
||||
validator.schema = {'foo': {'unknown': 'rule'}}
|
||||
msg = str({'foo': [{'type': ['Unsupported types: unknown']}]})
|
||||
with pytest.raises(SchemaError, match=re.escape(msg)):
|
||||
validator.schema = {'foo': {'type': 'unknown'}}
|
||||
|
||||
|
||||
def test_bad_schema_definition(validator):
|
||||
field = 'name'
|
||||
message = str({field: ['must be of dict type']})
|
||||
with pytest.raises(SchemaError, message=message):
|
||||
msg = str({field: ['must be of dict type']})
|
||||
with pytest.raises(SchemaError, match=re.escape(msg)):
|
||||
validator.schema = {field: 'this should really be a dict'}
|
||||
|
||||
|
||||
@@ -61,14 +61,14 @@ def test_normalization_rules_are_invalid_in_of_rules():
|
||||
def test_anyof_allof_schema_validate():
|
||||
# make sure schema with 'anyof' and 'allof' constraints are checked
|
||||
# correctly
|
||||
schema = {'doc': {'type': 'dict',
|
||||
'anyof': [
|
||||
{'schema': [{'param': {'type': 'number'}}]}]}}
|
||||
schema = {
|
||||
'doc': {'type': 'dict', 'anyof': [{'schema': [{'param': {'type': 'number'}}]}]}
|
||||
}
|
||||
assert_schema_error({'doc': 'this is my document'}, schema)
|
||||
|
||||
schema = {'doc': {'type': 'dict',
|
||||
'allof': [
|
||||
{'schema': [{'param': {'type': 'number'}}]}]}}
|
||||
schema = {
|
||||
'doc': {'type': 'dict', 'allof': [{'schema': [{'param': {'type': 'number'}}]}]}
|
||||
}
|
||||
assert_schema_error({'doc': 'this is my document'}, schema)
|
||||
|
||||
|
||||
@@ -88,24 +88,87 @@ def test_validated_schema_cache():
|
||||
v = Validator({'foozifix': {'coerce': int}})
|
||||
assert len(v._valid_schemas) == cache_size
|
||||
|
||||
max_cache_size = 147
|
||||
assert cache_size <= max_cache_size, \
|
||||
"There's an unexpected high amount (%s) of cached valid " \
|
||||
"definition schemas. Unless you added further tests, " \
|
||||
"there are good chances that something is wrong. " \
|
||||
"If you added tests with new schemas, you can try to " \
|
||||
"adjust the variable `max_cache_size` according to " \
|
||||
max_cache_size = 160
|
||||
assert cache_size <= max_cache_size, (
|
||||
"There's an unexpected high amount (%s) of cached valid "
|
||||
"definition schemas. Unless you added further tests, "
|
||||
"there are good chances that something is wrong. "
|
||||
"If you added tests with new schemas, you can try to "
|
||||
"adjust the variable `max_cache_size` according to "
|
||||
"the added schemas." % cache_size
|
||||
)
|
||||
|
||||
|
||||
def test_expansion_in_nested_schema():
|
||||
schema = {'detroit': {'schema': {'anyof_regex': ['^Aladdin', 'Sane$']}}}
|
||||
v = Validator(schema)
|
||||
assert (v.schema['detroit']['schema'] ==
|
||||
{'anyof': [{'regex': '^Aladdin'}, {'regex': 'Sane$'}]})
|
||||
assert v.schema['detroit']['schema'] == {
|
||||
'anyof': [{'regex': '^Aladdin'}, {'regex': 'Sane$'}]
|
||||
}
|
||||
|
||||
|
||||
def test_unvalidated_schema_can_be_copied():
|
||||
schema = UnvalidatedSchema()
|
||||
schema_copy = schema.copy()
|
||||
assert schema_copy == schema
|
||||
|
||||
|
||||
# TODO remove with next major release
|
||||
def test_deprecated_rule_names_in_valueschema():
|
||||
def check_with(field, value, error):
|
||||
pass
|
||||
|
||||
schema = {
|
||||
"field_1": {
|
||||
"type": "dict",
|
||||
"valueschema": {
|
||||
"type": "dict",
|
||||
"keyschema": {"type": "string"},
|
||||
"valueschema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"field_2": {
|
||||
"type": "list",
|
||||
"items": [
|
||||
{"keyschema": {}},
|
||||
{"validator": check_with},
|
||||
{"valueschema": {}},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
validator = Validator(schema)
|
||||
|
||||
assert validator.schema == {
|
||||
"field_1": {
|
||||
"type": "dict",
|
||||
"valuesrules": {
|
||||
"type": "dict",
|
||||
"keysrules": {"type": "string"},
|
||||
"valuesrules": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"field_2": {
|
||||
"type": "list",
|
||||
"items": [
|
||||
{"keysrules": {}},
|
||||
{"check_with": check_with},
|
||||
{"valuesrules": {}},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_anyof_check_with():
|
||||
def foo(field, value, error):
|
||||
pass
|
||||
|
||||
def bar(field, value, error):
|
||||
pass
|
||||
|
||||
schema = {'field': {'anyof_check_with': [foo, bar]}}
|
||||
validator = Validator(schema)
|
||||
|
||||
assert validator.schema == {
|
||||
'field': {'anyof': [{'check_with': foo}, {'check_with': bar}]}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
from cerberus.utils import compare_paths_lt
|
||||
|
||||
|
||||
def test_compare_paths():
|
||||
lesser = ('a_dict', 'keysrules')
|
||||
greater = ('a_dict', 'valuesrules')
|
||||
assert compare_paths_lt(lesser, greater)
|
||||
|
||||
lesser += ('type',)
|
||||
greater += ('regex',)
|
||||
assert compare_paths_lt(lesser, greater)
|
||||
+826
-457
File diff suppressed because it is too large
Load Diff
Vendored
+35
-24
@@ -1,12 +1,11 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from collections import Mapping, namedtuple, Sequence
|
||||
from collections import namedtuple
|
||||
|
||||
from cerberus.platform import _int_types, _str_type
|
||||
from cerberus.platform import _int_types, _str_type, Mapping, Sequence, Set
|
||||
|
||||
|
||||
TypeDefinition = namedtuple('TypeDefinition',
|
||||
'name,included_types,excluded_types')
|
||||
TypeDefinition = namedtuple('TypeDefinition', 'name,included_types,excluded_types')
|
||||
"""
|
||||
This class is used to define types that can be used as value in the
|
||||
:attr:`~cerberus.Validator.types_mapping` property.
|
||||
@@ -19,19 +18,33 @@ contained in ``excluded_types``.
|
||||
|
||||
|
||||
def compare_paths_lt(x, y):
|
||||
for i in range(min(len(x), len(y))):
|
||||
if isinstance(x[i], type(y[i])):
|
||||
if x[i] != y[i]:
|
||||
return x[i] < y[i]
|
||||
elif isinstance(x[i], _int_types):
|
||||
min_length = min(len(x), len(y))
|
||||
|
||||
if x[:min_length] == y[:min_length]:
|
||||
return len(x) == min_length
|
||||
|
||||
for i in range(min_length):
|
||||
a, b = x[i], y[i]
|
||||
|
||||
for _type in (_int_types, _str_type, tuple):
|
||||
if isinstance(a, _type):
|
||||
if isinstance(b, _type):
|
||||
break
|
||||
else:
|
||||
return True
|
||||
|
||||
if a == b:
|
||||
continue
|
||||
elif a < b:
|
||||
return True
|
||||
elif isinstance(y[i], _int_types):
|
||||
else:
|
||||
return False
|
||||
return len(x) < len(y)
|
||||
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
def drop_item_from_tuple(t, i):
|
||||
return t[:i] + t[i + 1:]
|
||||
return t[:i] + t[i + 1 :]
|
||||
|
||||
|
||||
def get_Validator_class():
|
||||
@@ -50,26 +63,24 @@ def mapping_to_frozenset(mapping):
|
||||
equal. As it is used to identify equality of schemas, this can be
|
||||
considered okay as definitions are semantically equal regardless the
|
||||
container type. """
|
||||
mapping = mapping.copy()
|
||||
|
||||
aggregation = {}
|
||||
|
||||
for key, value in mapping.items():
|
||||
if isinstance(value, Mapping):
|
||||
mapping[key] = mapping_to_frozenset(value)
|
||||
aggregation[key] = mapping_to_frozenset(value)
|
||||
elif isinstance(value, Sequence):
|
||||
value = list(value)
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, Mapping):
|
||||
value[i] = mapping_to_frozenset(item)
|
||||
mapping[key] = tuple(value)
|
||||
return frozenset(mapping.items())
|
||||
aggregation[key] = tuple(value)
|
||||
elif isinstance(value, Set):
|
||||
aggregation[key] = frozenset(value)
|
||||
else:
|
||||
aggregation[key] = value
|
||||
|
||||
|
||||
def isclass(obj):
|
||||
try:
|
||||
issubclass(obj, object)
|
||||
except TypeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return frozenset(aggregation.items())
|
||||
|
||||
|
||||
def quote_string(value):
|
||||
|
||||
Vendored
+507
-299
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
from .core import where
|
||||
|
||||
__version__ = "2018.11.29"
|
||||
__version__ = "2019.03.09"
|
||||
|
||||
Vendored
+146
@@ -4510,3 +4510,149 @@ Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh
|
||||
jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw
|
||||
3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI
|
||||
# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI
|
||||
# Label: "emSign Root CA - G1"
|
||||
# Serial: 235931866688319308814040
|
||||
# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac
|
||||
# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c
|
||||
# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD
|
||||
VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU
|
||||
ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH
|
||||
MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO
|
||||
MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv
|
||||
Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN
|
||||
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz
|
||||
f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO
|
||||
8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq
|
||||
d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM
|
||||
tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt
|
||||
Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB
|
||||
o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD
|
||||
AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x
|
||||
PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM
|
||||
wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d
|
||||
GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH
|
||||
6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby
|
||||
RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
|
||||
iN66zB+Afko=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI
|
||||
# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI
|
||||
# Label: "emSign ECC Root CA - G3"
|
||||
# Serial: 287880440101571086945156
|
||||
# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40
|
||||
# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1
|
||||
# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
|
||||
EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
|
||||
bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
|
||||
RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
|
||||
TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
|
||||
b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
|
||||
djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
|
||||
WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
|
||||
fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
|
||||
zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
|
||||
hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
|
||||
CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
|
||||
+JbNR6iC8hZVdyR+EhCVBCyj
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI
|
||||
# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI
|
||||
# Label: "emSign Root CA - C1"
|
||||
# Serial: 825510296613316004955058
|
||||
# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68
|
||||
# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01
|
||||
# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG
|
||||
A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg
|
||||
SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw
|
||||
MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
|
||||
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v
|
||||
dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ
|
||||
BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ
|
||||
HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH
|
||||
3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH
|
||||
GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c
|
||||
xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1
|
||||
aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq
|
||||
TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87
|
||||
/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4
|
||||
kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG
|
||||
YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT
|
||||
+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo
|
||||
WXzhriKi4gp6D/piq1JM4fHfyr6DDUI=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI
|
||||
# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI
|
||||
# Label: "emSign ECC Root CA - C3"
|
||||
# Serial: 582948710642506000014504
|
||||
# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5
|
||||
# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66
|
||||
# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG
|
||||
EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx
|
||||
IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw
|
||||
MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
|
||||
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND
|
||||
IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci
|
||||
MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti
|
||||
sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O
|
||||
BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
|
||||
Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c
|
||||
3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J
|
||||
0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post
|
||||
# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post
|
||||
# Label: "Hongkong Post Root CA 3"
|
||||
# Serial: 46170865288971385588281144162979347873371282084
|
||||
# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0
|
||||
# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02
|
||||
# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
|
||||
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
|
||||
SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n
|
||||
a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5
|
||||
NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT
|
||||
CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u
|
||||
Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
|
||||
AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO
|
||||
dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI
|
||||
VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV
|
||||
9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY
|
||||
2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY
|
||||
vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt
|
||||
bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb
|
||||
x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+
|
||||
l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK
|
||||
TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj
|
||||
Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP
|
||||
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e
|
||||
i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw
|
||||
DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG
|
||||
7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk
|
||||
MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr
|
||||
gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk
|
||||
GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS
|
||||
3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm
|
||||
Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+
|
||||
l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c
|
||||
JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP
|
||||
L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
|
||||
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
|
||||
mpv0
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
Vendored
-5
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@@ -14,7 +13,3 @@ def where():
|
||||
f = os.path.dirname(__file__)
|
||||
|
||||
return os.path.join(f, 'cacert.pem')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(where())
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ from click_completion.core import completion_configuration, get_code, install, s
|
||||
from click_completion.lib import get_auto_shell
|
||||
from click_completion.patch import patch as _patch
|
||||
|
||||
__version__ = '0.5.0'
|
||||
__version__ = '0.5.1'
|
||||
|
||||
_initialized = False
|
||||
|
||||
|
||||
+8
-12
@@ -131,8 +131,9 @@ def get_choices(cli, prog_name, args, incomplete):
|
||||
choices.append((opt, None))
|
||||
if isinstance(ctx.command, MultiCommand):
|
||||
for name in ctx.command.list_commands(ctx):
|
||||
if match(name, incomplete):
|
||||
choices.append((name, ctx.command.get_command_short_help(ctx, name)))
|
||||
command = ctx.command.get_command(ctx, name)
|
||||
if match(name, incomplete) and not command.hidden:
|
||||
choices.append((name, command.get_short_help_str()))
|
||||
|
||||
for item, help in choices:
|
||||
yield (item, help)
|
||||
@@ -201,7 +202,7 @@ def do_fish_complete(cli, prog_name):
|
||||
|
||||
for item, help in get_choices(cli, prog_name, args, incomplete):
|
||||
if help:
|
||||
echo("%s\t%s" % (item, re.sub('\s', ' ', help)))
|
||||
echo("%s\t%s" % (item, re.sub(r'\s', ' ', help)))
|
||||
else:
|
||||
echo(item)
|
||||
|
||||
@@ -232,11 +233,11 @@ def do_zsh_complete(cli, prog_name):
|
||||
incomplete = ''
|
||||
|
||||
def escape(s):
|
||||
return s.replace('"', '""').replace("'", "''").replace('$', '\\$')
|
||||
return s.replace('"', '""').replace("'", "''").replace('$', '\\$').replace('`', '\\`')
|
||||
res = []
|
||||
for item, help in get_choices(cli, prog_name, args, incomplete):
|
||||
if help:
|
||||
res.append('"%s"\:"%s"' % (escape(item), escape(help)))
|
||||
res.append(r'"%s"\:"%s"' % (escape(item), escape(help)))
|
||||
else:
|
||||
res.append('"%s"' % escape(item))
|
||||
if res:
|
||||
@@ -349,13 +350,8 @@ def install(shell=None, prog_name=None, env_name=None, path=None, append=None, e
|
||||
path = path or os.path.expanduser('~') + '/.bash_completion'
|
||||
mode = mode or 'a'
|
||||
elif shell == 'zsh':
|
||||
ohmyzsh = os.path.expanduser('~') + '/.oh-my-zsh'
|
||||
if os.path.exists(ohmyzsh):
|
||||
path = path or ohmyzsh + '/completions/_%s' % prog_name
|
||||
mode = mode or 'w'
|
||||
else:
|
||||
path = path or os.path.expanduser('~') + '/.zshrc'
|
||||
mode = mode or 'a'
|
||||
path = path or os.path.expanduser('~') + '/.zshrc'
|
||||
mode = mode or 'a'
|
||||
elif shell == 'powershell':
|
||||
subprocess.check_call(['powershell', 'Set-ExecutionPolicy Unrestricted -Scope CurrentUser'])
|
||||
path = path or subprocess.check_output(['powershell', '-NoProfile', 'echo $profile']).strip() if install else ''
|
||||
|
||||
-1
@@ -3,6 +3,5 @@ _{{prog_name}}() {
|
||||
eval $(env COMMANDLINE="${words[1,$CURRENT]}" {{complete_var}}=complete-zsh {% for k, v in extra_env.items() %} {{k}}={{v}}{% endfor %} {{prog_name}})
|
||||
}
|
||||
if [[ "$(basename -- ${(%):-%x})" != "_{{prog_name}}" ]]; then
|
||||
autoload -U compinit && compinit
|
||||
compdef _{{prog_name}} {{prog_name}}
|
||||
fi
|
||||
|
||||
Vendored
+1
-1
@@ -6,7 +6,7 @@
|
||||
#
|
||||
import logging
|
||||
|
||||
__version__ = '0.2.8'
|
||||
__version__ = '0.2.9'
|
||||
|
||||
class DistlibException(Exception):
|
||||
pass
|
||||
|
||||
Vendored
+1
-1
@@ -22,7 +22,7 @@ from .util import cached_property, zip_dir, ServerProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_INDEX = 'https://pypi.python.org/pypi'
|
||||
DEFAULT_INDEX = 'https://pypi.org/pypi'
|
||||
DEFAULT_REALM = 'pypi'
|
||||
|
||||
class PackageIndex(object):
|
||||
|
||||
Vendored
+3
-3
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
HASHER_HASH = re.compile(r'^(\w+)=([a-f0-9]+)')
|
||||
CHARSET = re.compile(r';\s*charset\s*=\s*(.*)\s*$', re.I)
|
||||
HTML_CONTENT_TYPE = re.compile('text/html|application/x(ht)?ml')
|
||||
DEFAULT_INDEX = 'https://pypi.python.org/pypi'
|
||||
DEFAULT_INDEX = 'https://pypi.org/pypi'
|
||||
|
||||
def get_all_distribution_names(url=None):
|
||||
"""
|
||||
@@ -197,7 +197,7 @@ class Locator(object):
|
||||
is_downloadable = basename.endswith(self.downloadable_extensions)
|
||||
if is_wheel:
|
||||
compatible = is_compatible(Wheel(basename), self.wheel_tags)
|
||||
return (t.scheme == 'https', 'pypi.python.org' in t.netloc,
|
||||
return (t.scheme == 'https', 'pypi.org' in t.netloc,
|
||||
is_downloadable, is_wheel, compatible, basename)
|
||||
|
||||
def prefer_url(self, url1, url2):
|
||||
@@ -1049,7 +1049,7 @@ class AggregatingLocator(Locator):
|
||||
# versions which don't conform to PEP 426 / PEP 440.
|
||||
default_locator = AggregatingLocator(
|
||||
JSONLocator(),
|
||||
SimpleScrapingLocator('https://pypi.python.org/simple/',
|
||||
SimpleScrapingLocator('https://pypi.org/simple/',
|
||||
timeout=3.0),
|
||||
scheme='legacy')
|
||||
|
||||
|
||||
Vendored
+5
-3
@@ -91,9 +91,11 @@ _426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
|
||||
_426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
|
||||
'Setup-Requires-Dist', 'Extension')
|
||||
|
||||
# See issue #106: Sometimes 'Requires' occurs wrongly in the metadata. Include
|
||||
# it in the tuple literal below to allow it (for now)
|
||||
_566_FIELDS = _426_FIELDS + ('Description-Content-Type', 'Requires')
|
||||
# See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in
|
||||
# the metadata. Include them in the tuple literal below to allow them
|
||||
# (for now).
|
||||
_566_FIELDS = _426_FIELDS + ('Description-Content-Type',
|
||||
'Requires', 'Provides')
|
||||
|
||||
_566_MARKERS = ('Description-Content-Type',)
|
||||
|
||||
|
||||
Vendored
+6
-20
@@ -39,27 +39,12 @@ _DEFAULT_MANIFEST = '''
|
||||
# check if Python is called on the first line with this expression
|
||||
FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
|
||||
SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from %(module)s import %(import_name)s
|
||||
if __name__ == '__main__':
|
||||
import sys, re
|
||||
|
||||
def _resolve(module, func):
|
||||
__import__(module)
|
||||
mod = sys.modules[module]
|
||||
parts = func.split('.')
|
||||
result = getattr(mod, parts.pop(0))
|
||||
for p in parts:
|
||||
result = getattr(result, p)
|
||||
return result
|
||||
|
||||
try:
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||
|
||||
func = _resolve('%(module)s', '%(func)s')
|
||||
rc = func() # None interpreted as 0
|
||||
except Exception as e: # only supporting Python >= 2.6
|
||||
sys.stderr.write('%%s\n' %% e)
|
||||
rc = 1
|
||||
sys.exit(rc)
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(%(func)s())
|
||||
'''
|
||||
|
||||
|
||||
@@ -225,6 +210,7 @@ class ScriptMaker(object):
|
||||
|
||||
def _get_script_text(self, entry):
|
||||
return self.script_template % dict(module=entry.prefix,
|
||||
import_name=entry.suffix.split('.')[0],
|
||||
func=entry.suffix)
|
||||
|
||||
manifest = _DEFAULT_MANIFEST
|
||||
|
||||
Vendored
+5
-1
@@ -804,11 +804,15 @@ def ensure_slash(s):
|
||||
def parse_credentials(netloc):
|
||||
username = password = None
|
||||
if '@' in netloc:
|
||||
prefix, netloc = netloc.split('@', 1)
|
||||
prefix, netloc = netloc.rsplit('@', 1)
|
||||
if ':' not in prefix:
|
||||
username = prefix
|
||||
else:
|
||||
username, password = prefix.split(':', 1)
|
||||
if username:
|
||||
username = unquote(username)
|
||||
if password:
|
||||
password = unquote(password)
|
||||
return username, password, netloc
|
||||
|
||||
|
||||
|
||||
Vendored
+23
-7
@@ -433,6 +433,22 @@ class Wheel(object):
|
||||
self.build_zip(pathname, archive_paths)
|
||||
return pathname
|
||||
|
||||
def skip_entry(self, arcname):
|
||||
"""
|
||||
Determine whether an archive entry should be skipped when verifying
|
||||
or installing.
|
||||
"""
|
||||
# The signature file won't be in RECORD,
|
||||
# and we don't currently don't do anything with it
|
||||
# We also skip directories, as they won't be in RECORD
|
||||
# either. See:
|
||||
#
|
||||
# https://github.com/pypa/wheel/issues/294
|
||||
# https://github.com/pypa/wheel/issues/287
|
||||
# https://github.com/pypa/wheel/pull/289
|
||||
#
|
||||
return arcname.endswith(('/', '/RECORD.jws'))
|
||||
|
||||
def install(self, paths, maker, **kwargs):
|
||||
"""
|
||||
Install a wheel to the specified paths. If kwarg ``warner`` is
|
||||
@@ -514,9 +530,7 @@ class Wheel(object):
|
||||
u_arcname = arcname
|
||||
else:
|
||||
u_arcname = arcname.decode('utf-8')
|
||||
# The signature file won't be in RECORD,
|
||||
# and we don't currently don't do anything with it
|
||||
if u_arcname.endswith('/RECORD.jws'):
|
||||
if self.skip_entry(u_arcname):
|
||||
continue
|
||||
row = records[u_arcname]
|
||||
if row[2] and str(zinfo.file_size) != row[2]:
|
||||
@@ -786,13 +800,15 @@ class Wheel(object):
|
||||
u_arcname = arcname
|
||||
else:
|
||||
u_arcname = arcname.decode('utf-8')
|
||||
if '..' in u_arcname:
|
||||
# See issue #115: some wheels have .. in their entries, but
|
||||
# in the filename ... e.g. __main__..py ! So the check is
|
||||
# updated to look for .. in the directory portions
|
||||
p = u_arcname.split('/')
|
||||
if '..' in p:
|
||||
raise DistlibException('invalid entry in '
|
||||
'wheel: %r' % u_arcname)
|
||||
|
||||
# The signature file won't be in RECORD,
|
||||
# and we don't currently don't do anything with it
|
||||
if u_arcname.endswith('/RECORD.jws'):
|
||||
if self.skip_entry(u_arcname):
|
||||
continue
|
||||
row = records[u_arcname]
|
||||
if row[2] and str(zinfo.file_size) != row[2]:
|
||||
|
||||
Vendored
+3
@@ -1,12 +1,15 @@
|
||||
from typing import Any, Optional
|
||||
from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values
|
||||
|
||||
|
||||
def load_ipython_extension(ipython):
|
||||
# type: (Any) -> None
|
||||
from .ipython import load_ipython_extension
|
||||
load_ipython_extension(ipython)
|
||||
|
||||
|
||||
def get_cli_string(path=None, action=None, key=None, value=None, quote=None):
|
||||
# type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str
|
||||
"""Returns a string suitable for running as a shell script.
|
||||
|
||||
Useful for converting a arguments passed to a fabric task
|
||||
|
||||
Vendored
+8
-1
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, List
|
||||
|
||||
try:
|
||||
import click
|
||||
@@ -22,6 +23,7 @@ from .version import __version__
|
||||
@click.version_option(version=__version__)
|
||||
@click.pass_context
|
||||
def cli(ctx, file, quote):
|
||||
# type: (click.Context, Any, Any) -> None
|
||||
'''This script is used to set, get or unset values from a .env file.'''
|
||||
ctx.obj = {}
|
||||
ctx.obj['FILE'] = file
|
||||
@@ -31,6 +33,7 @@ def cli(ctx, file, quote):
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def list(ctx):
|
||||
# type: (click.Context) -> None
|
||||
'''Display all the stored key/value.'''
|
||||
file = ctx.obj['FILE']
|
||||
dotenv_as_dict = dotenv_values(file)
|
||||
@@ -43,6 +46,7 @@ def list(ctx):
|
||||
@click.argument('key', required=True)
|
||||
@click.argument('value', required=True)
|
||||
def set(ctx, key, value):
|
||||
# type: (click.Context, Any, Any) -> None
|
||||
'''Store the given key/value.'''
|
||||
file = ctx.obj['FILE']
|
||||
quote = ctx.obj['QUOTE']
|
||||
@@ -57,6 +61,7 @@ def set(ctx, key, value):
|
||||
@click.pass_context
|
||||
@click.argument('key', required=True)
|
||||
def get(ctx, key):
|
||||
# type: (click.Context, Any) -> None
|
||||
'''Retrieve the value for the given key.'''
|
||||
file = ctx.obj['FILE']
|
||||
stored_value = get_key(file, key)
|
||||
@@ -70,6 +75,7 @@ def get(ctx, key):
|
||||
@click.pass_context
|
||||
@click.argument('key', required=True)
|
||||
def unset(ctx, key):
|
||||
# type: (click.Context, Any) -> None
|
||||
'''Removes the given key.'''
|
||||
file = ctx.obj['FILE']
|
||||
quote = ctx.obj['QUOTE']
|
||||
@@ -84,13 +90,14 @@ def unset(ctx, key):
|
||||
@click.pass_context
|
||||
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
|
||||
def run(ctx, commandline):
|
||||
# type: (click.Context, List[str]) -> None
|
||||
"""Run command with environment variables present."""
|
||||
file = ctx.obj['FILE']
|
||||
dotenv_as_dict = dotenv_values(file)
|
||||
if not commandline:
|
||||
click.echo('No command given.')
|
||||
exit(1)
|
||||
ret = run_command(commandline, dotenv_as_dict)
|
||||
ret = run_command(commandline, dotenv_as_dict) # type: ignore
|
||||
exit(ret)
|
||||
|
||||
|
||||
|
||||
Vendored
+6
-7
@@ -1,9 +1,8 @@
|
||||
import sys
|
||||
try:
|
||||
from StringIO import StringIO # noqa
|
||||
except ImportError:
|
||||
from io import StringIO # noqa
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
WIN = sys.platform.startswith('win')
|
||||
text_type = unicode if PY2 else str # noqa
|
||||
if sys.version_info >= (3, 0):
|
||||
from io import StringIO # noqa
|
||||
else:
|
||||
from StringIO import StringIO # noqa
|
||||
|
||||
PY2 = sys.version_info[0] == 2 # type: bool
|
||||
|
||||
Vendored
-54
@@ -1,54 +0,0 @@
|
||||
import os
|
||||
|
||||
|
||||
class UndefinedValueError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Undefined(object):
|
||||
"""Class to represent undefined type. """
|
||||
pass
|
||||
|
||||
|
||||
# Reference instance to represent undefined values
|
||||
undefined = Undefined()
|
||||
|
||||
|
||||
def _cast_boolean(value):
|
||||
"""
|
||||
Helper to convert config values to boolean as ConfigParser do.
|
||||
"""
|
||||
_BOOLEANS = {'1': True, 'yes': True, 'true': True, 'on': True,
|
||||
'0': False, 'no': False, 'false': False, 'off': False, '': False}
|
||||
value = str(value)
|
||||
if value.lower() not in _BOOLEANS:
|
||||
raise ValueError('Not a boolean: %s' % value)
|
||||
|
||||
return _BOOLEANS[value.lower()]
|
||||
|
||||
|
||||
def getenv(option, default=undefined, cast=undefined):
|
||||
"""
|
||||
Return the value for option or default if defined.
|
||||
"""
|
||||
|
||||
# We can't avoid __contains__ because value may be empty.
|
||||
if option in os.environ:
|
||||
value = os.environ[option]
|
||||
else:
|
||||
if isinstance(default, Undefined):
|
||||
raise UndefinedValueError('{} not found. Declare it as envvar or define a default value.'.format(option))
|
||||
|
||||
value = default
|
||||
|
||||
if isinstance(cast, Undefined):
|
||||
return value
|
||||
|
||||
if cast is bool:
|
||||
value = _cast_boolean(value)
|
||||
elif cast is list:
|
||||
value = [x for x in value.split(',') if x]
|
||||
else:
|
||||
value = cast(value)
|
||||
|
||||
return value
|
||||
Vendored
+3
-3
@@ -1,8 +1,8 @@
|
||||
from __future__ import print_function
|
||||
|
||||
from IPython.core.magic import Magics, line_magic, magics_class
|
||||
from IPython.core.magic_arguments import (argument, magic_arguments,
|
||||
parse_argstring)
|
||||
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
|
||||
from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
|
||||
parse_argstring) # type: ignore
|
||||
|
||||
from .main import find_dotenv, load_dotenv
|
||||
|
||||
|
||||
Vendored
+84
-30
@@ -9,13 +9,26 @@ import shutil
|
||||
import sys
|
||||
from subprocess import Popen
|
||||
import tempfile
|
||||
from typing import (Any, Dict, Iterator, List, Match, NamedTuple, Optional, # noqa
|
||||
Pattern, Union, TYPE_CHECKING, Text, IO, Tuple) # noqa
|
||||
import warnings
|
||||
from collections import OrderedDict, namedtuple
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .compat import StringIO, PY2, WIN, text_type
|
||||
from .compat import StringIO, PY2
|
||||
|
||||
__posix_variable = re.compile(r'\$\{[^\}]*\}')
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if sys.version_info >= (3, 6):
|
||||
_PathLike = os.PathLike
|
||||
else:
|
||||
_PathLike = Text
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
_StringIO = StringIO
|
||||
else:
|
||||
_StringIO = StringIO[Text]
|
||||
|
||||
__posix_variable = re.compile(r'\$\{[^\}]*\}') # type: Pattern[Text]
|
||||
|
||||
_binding = re.compile(
|
||||
r"""
|
||||
@@ -42,22 +55,27 @@ _binding = re.compile(
|
||||
)
|
||||
""".format(r'[^\S\r\n]'),
|
||||
re.MULTILINE | re.VERBOSE,
|
||||
)
|
||||
) # type: Pattern[Text]
|
||||
|
||||
_escape_sequence = re.compile(r"\\[\\'\"abfnrtv]")
|
||||
_escape_sequence = re.compile(r"\\[\\'\"abfnrtv]") # type: Pattern[Text]
|
||||
|
||||
|
||||
Binding = namedtuple('Binding', 'key value original')
|
||||
Binding = NamedTuple("Binding", [("key", Optional[Text]),
|
||||
("value", Optional[Text]),
|
||||
("original", Text)])
|
||||
|
||||
|
||||
def decode_escapes(string):
|
||||
# type: (Text) -> Text
|
||||
def decode_match(match):
|
||||
return codecs.decode(match.group(0), 'unicode-escape')
|
||||
# type: (Match[Text]) -> Text
|
||||
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
|
||||
|
||||
return _escape_sequence.sub(decode_match, string)
|
||||
|
||||
|
||||
def is_surrounded_by(string, char):
|
||||
# type: (Text, Text) -> bool
|
||||
return (
|
||||
len(string) > 1
|
||||
and string[0] == string[-1] == char
|
||||
@@ -65,7 +83,9 @@ def is_surrounded_by(string, char):
|
||||
|
||||
|
||||
def parse_binding(string, position):
|
||||
# type: (Text, int) -> Tuple[Binding, int]
|
||||
match = _binding.match(string, position)
|
||||
assert match is not None
|
||||
(matched, key, value) = match.groups()
|
||||
if key is None or value is None:
|
||||
key = None
|
||||
@@ -80,6 +100,7 @@ def parse_binding(string, position):
|
||||
|
||||
|
||||
def parse_stream(stream):
|
||||
# type:(IO[Text]) -> Iterator[Binding]
|
||||
string = stream.read()
|
||||
position = 0
|
||||
length = len(string)
|
||||
@@ -88,26 +109,41 @@ def parse_stream(stream):
|
||||
yield binding
|
||||
|
||||
|
||||
def to_env(text):
|
||||
# type: (Text) -> str
|
||||
"""
|
||||
Encode a string the same way whether it comes from the environment or a `.env` file.
|
||||
"""
|
||||
if PY2:
|
||||
return text.encode(sys.getfilesystemencoding() or "utf-8")
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
class DotEnv():
|
||||
|
||||
def __init__(self, dotenv_path, verbose=False):
|
||||
self.dotenv_path = dotenv_path
|
||||
self._dict = None
|
||||
self.verbose = verbose
|
||||
def __init__(self, dotenv_path, verbose=False, encoding=None):
|
||||
# type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text]) -> None
|
||||
self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO]
|
||||
self._dict = None # type: Optional[Dict[Text, Text]]
|
||||
self.verbose = verbose # type: bool
|
||||
self.encoding = encoding # type: Union[None, Text]
|
||||
|
||||
@contextmanager
|
||||
def _get_stream(self):
|
||||
# type: () -> Iterator[IO[Text]]
|
||||
if isinstance(self.dotenv_path, StringIO):
|
||||
yield self.dotenv_path
|
||||
elif os.path.isfile(self.dotenv_path):
|
||||
with io.open(self.dotenv_path) as stream:
|
||||
with io.open(self.dotenv_path, encoding=self.encoding) as stream:
|
||||
yield stream
|
||||
else:
|
||||
if self.verbose:
|
||||
warnings.warn("File doesn't exist {}".format(self.dotenv_path))
|
||||
warnings.warn("File doesn't exist {}".format(self.dotenv_path)) # type: ignore
|
||||
yield StringIO('')
|
||||
|
||||
def dict(self):
|
||||
# type: () -> Dict[Text, Text]
|
||||
"""Return dotenv as dict"""
|
||||
if self._dict:
|
||||
return self._dict
|
||||
@@ -117,29 +153,26 @@ class DotEnv():
|
||||
return self._dict
|
||||
|
||||
def parse(self):
|
||||
# type: () -> Iterator[Tuple[Text, Text]]
|
||||
with self._get_stream() as stream:
|
||||
for mapping in parse_stream(stream):
|
||||
if mapping.key is not None and mapping.value is not None:
|
||||
yield mapping.key, mapping.value
|
||||
|
||||
def set_as_environment_variables(self, override=False):
|
||||
# type: (bool) -> bool
|
||||
"""
|
||||
Load the current dotenv as system environemt variable.
|
||||
"""
|
||||
for k, v in self.dict().items():
|
||||
if k in os.environ and not override:
|
||||
continue
|
||||
# With Python2 on Windows, force environment variables to str to avoid
|
||||
# "TypeError: environment can only contain strings" in Python's subprocess.py.
|
||||
if PY2 and WIN:
|
||||
if isinstance(k, text_type) or isinstance(v, text_type):
|
||||
k = k.encode('ascii')
|
||||
v = v.encode('ascii')
|
||||
os.environ[k] = v
|
||||
os.environ[to_env(k)] = to_env(v)
|
||||
|
||||
return True
|
||||
|
||||
def get(self, key):
|
||||
# type: (Text) -> Optional[Text]
|
||||
"""
|
||||
"""
|
||||
data = self.dict()
|
||||
@@ -148,10 +181,13 @@ class DotEnv():
|
||||
return data[key]
|
||||
|
||||
if self.verbose:
|
||||
warnings.warn("key %s not found in %s." % (key, self.dotenv_path))
|
||||
warnings.warn("key %s not found in %s." % (key, self.dotenv_path)) # type: ignore
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_key(dotenv_path, key_to_get):
|
||||
# type: (Union[Text, _PathLike], Text) -> Optional[Text]
|
||||
"""
|
||||
Gets the value of a given key from the given .env
|
||||
|
||||
@@ -162,10 +198,11 @@ def get_key(dotenv_path, key_to_get):
|
||||
|
||||
@contextmanager
|
||||
def rewrite(path):
|
||||
# type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
|
||||
with io.open(path) as source:
|
||||
yield (source, dest)
|
||||
yield (source, dest) # type: ignore
|
||||
except BaseException:
|
||||
if os.path.isfile(dest.name):
|
||||
os.unlink(dest.name)
|
||||
@@ -175,6 +212,7 @@ def rewrite(path):
|
||||
|
||||
|
||||
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
|
||||
# type: (_PathLike, Text, Text, Text) -> Tuple[Optional[bool], Text, Text]
|
||||
"""
|
||||
Adds or Updates a key/value to the given .env
|
||||
|
||||
@@ -183,7 +221,7 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
|
||||
"""
|
||||
value_to_set = value_to_set.strip("'").strip('"')
|
||||
if not os.path.exists(dotenv_path):
|
||||
warnings.warn("can't write to %s - it doesn't exist." % dotenv_path)
|
||||
warnings.warn("can't write to %s - it doesn't exist." % dotenv_path) # type: ignore
|
||||
return None, key_to_set, value_to_set
|
||||
|
||||
if " " in value_to_set:
|
||||
@@ -207,6 +245,7 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
|
||||
|
||||
|
||||
def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
|
||||
# type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text]
|
||||
"""
|
||||
Removes a given key from the given .env
|
||||
|
||||
@@ -214,7 +253,7 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
|
||||
If the given key doesn't exist in the .env, fails
|
||||
"""
|
||||
if not os.path.exists(dotenv_path):
|
||||
warnings.warn("can't delete from %s - it doesn't exist." % dotenv_path)
|
||||
warnings.warn("can't delete from %s - it doesn't exist." % dotenv_path) # type: ignore
|
||||
return None, key_to_unset
|
||||
|
||||
removed = False
|
||||
@@ -226,14 +265,16 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
|
||||
dest.write(mapping.original)
|
||||
|
||||
if not removed:
|
||||
warnings.warn("key %s not removed from %s - key doesn't exist." % (key_to_unset, dotenv_path))
|
||||
warnings.warn("key %s not removed from %s - key doesn't exist." % (key_to_unset, dotenv_path)) # type: ignore
|
||||
return None, key_to_unset
|
||||
|
||||
return removed, key_to_unset
|
||||
|
||||
|
||||
def resolve_nested_variables(values):
|
||||
# type: (Dict[Text, Text]) -> Dict[Text, Text]
|
||||
def _replacement(name):
|
||||
# type: (Text) -> Text
|
||||
"""
|
||||
get appropriate value for a variable name.
|
||||
first search in environ, if not found,
|
||||
@@ -243,6 +284,7 @@ def resolve_nested_variables(values):
|
||||
return ret
|
||||
|
||||
def _re_sub_callback(match_object):
|
||||
# type: (Match[Text]) -> Text
|
||||
"""
|
||||
From a match object gets the variable name and returns
|
||||
the correct replacement
|
||||
@@ -258,6 +300,7 @@ def resolve_nested_variables(values):
|
||||
|
||||
|
||||
def _walk_to_root(path):
|
||||
# type: (Text) -> Iterator[Text]
|
||||
"""
|
||||
Yield directories starting from the given directory up to the root
|
||||
"""
|
||||
@@ -276,6 +319,7 @@ def _walk_to_root(path):
|
||||
|
||||
|
||||
def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
|
||||
# type: (Text, bool, bool) -> Text
|
||||
"""
|
||||
Search in increasingly higher folders for the given file
|
||||
|
||||
@@ -288,7 +332,14 @@ def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
|
||||
# will work for .py files
|
||||
frame = sys._getframe()
|
||||
# find first frame that is outside of this file
|
||||
while frame.f_code.co_filename == __file__:
|
||||
if PY2 and not __file__.endswith('.py'):
|
||||
# in Python2 __file__ extension could be .pyc or .pyo (this doesn't account
|
||||
# for edge case of Python compiled for non-standard extension)
|
||||
current_file = __file__.rsplit('.', 1)[0] + '.py'
|
||||
else:
|
||||
current_file = __file__
|
||||
|
||||
while frame.f_code.co_filename == current_file:
|
||||
frame = frame.f_back
|
||||
frame_filename = frame.f_code.co_filename
|
||||
path = os.path.dirname(os.path.abspath(frame_filename))
|
||||
@@ -304,17 +355,20 @@ def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
|
||||
return ''
|
||||
|
||||
|
||||
def load_dotenv(dotenv_path=None, stream=None, verbose=False, override=False):
|
||||
def load_dotenv(dotenv_path=None, stream=None, verbose=False, override=False, **kwargs):
|
||||
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Union[None, Text]) -> bool
|
||||
f = dotenv_path or stream or find_dotenv()
|
||||
return DotEnv(f, verbose=verbose).set_as_environment_variables(override=override)
|
||||
return DotEnv(f, verbose=verbose, **kwargs).set_as_environment_variables(override=override)
|
||||
|
||||
|
||||
def dotenv_values(dotenv_path=None, stream=None, verbose=False):
|
||||
def dotenv_values(dotenv_path=None, stream=None, verbose=False, **kwargs):
|
||||
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, Union[None, Text]) -> Dict[Text, Text]
|
||||
f = dotenv_path or stream or find_dotenv()
|
||||
return DotEnv(f, verbose=verbose).dict()
|
||||
return DotEnv(f, verbose=verbose, **kwargs).dict()
|
||||
|
||||
|
||||
def run_command(command, env):
|
||||
# type: (List[str], Dict[str, str]) -> int
|
||||
"""Run command in sub process.
|
||||
|
||||
Runs the command in a sub process with the variables from `env`
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
# Marker file for PEP 561
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "0.10.1"
|
||||
__version__ = "0.10.2"
|
||||
|
||||
Vendored
+4
-3
@@ -345,6 +345,7 @@ the pattern, the actual match represents the shortest successful match for
|
||||
|
||||
**Version history (in brief)**:
|
||||
|
||||
- 1.12.0 Do not assume closing brace when an opening one is found (thanks @mattsep)
|
||||
- 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
|
||||
@@ -415,7 +416,7 @@ See the end of the source file for the license of use.
|
||||
'''
|
||||
|
||||
from __future__ import absolute_import
|
||||
__version__ = '1.11.1'
|
||||
__version__ = '1.12.0'
|
||||
|
||||
# yes, I now have two problems
|
||||
import re
|
||||
@@ -431,7 +432,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def with_pattern(pattern, regex_group_count=None):
|
||||
"""Attach a regular expression pattern matcher to a custom type converter
|
||||
r"""Attach a regular expression pattern matcher to a custom type converter
|
||||
function.
|
||||
|
||||
This annotates the type converter with the :attr:`pattern` attribute.
|
||||
@@ -885,7 +886,7 @@ class Parser(object):
|
||||
e.append(r'\{')
|
||||
elif part == '}}':
|
||||
e.append(r'\}')
|
||||
elif part[0] == '{':
|
||||
elif part[0] == '{' and part[-1] == '}':
|
||||
# this will be a braces-delimited field to handle
|
||||
e.append(self._handle_field(part))
|
||||
else:
|
||||
|
||||
Vendored
+1
-1
@@ -75,7 +75,7 @@ if sys.platform != 'win32':
|
||||
from .pty_spawn import spawn, spawnu
|
||||
from .run import run, runu
|
||||
|
||||
__version__ = '4.6.0'
|
||||
__version__ = '4.7.0'
|
||||
__revision__ = ''
|
||||
__all__ = ['ExceptionPexpect', 'EOF', 'TIMEOUT', 'spawn', 'spawnu', 'run', 'runu',
|
||||
'which', 'split_command_line', '__version__', '__revision__']
|
||||
|
||||
Vendored
+22
-4
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import errno
|
||||
import signal
|
||||
|
||||
from pexpect import EOF
|
||||
|
||||
@@ -29,6 +30,23 @@ def expect_async(expecter, timeout=None):
|
||||
transport.pause_reading()
|
||||
return expecter.timeout(e)
|
||||
|
||||
@asyncio.coroutine
|
||||
def repl_run_command_async(repl, cmdlines, timeout=-1):
|
||||
res = []
|
||||
repl.child.sendline(cmdlines[0])
|
||||
for line in cmdlines[1:]:
|
||||
yield from repl._expect_prompt(timeout=timeout, async_=True)
|
||||
res.append(repl.child.before)
|
||||
repl.child.sendline(line)
|
||||
|
||||
# Command was fully submitted, now wait for the next prompt
|
||||
prompt_idx = yield from repl._expect_prompt(timeout=timeout, async_=True)
|
||||
if prompt_idx == 1:
|
||||
# We got the continuation prompt - command was incomplete
|
||||
repl.child.kill(signal.SIGINT)
|
||||
yield from repl._expect_prompt(timeout=1, async_=True)
|
||||
raise ValueError("Continuation prompt found - input was incomplete:")
|
||||
return u''.join(res + [repl.child.before])
|
||||
|
||||
class PatternWaiter(asyncio.Protocol):
|
||||
transport = None
|
||||
@@ -41,7 +59,7 @@ class PatternWaiter(asyncio.Protocol):
|
||||
if not self.fut.done():
|
||||
self.fut.set_result(result)
|
||||
self.transport.pause_reading()
|
||||
|
||||
|
||||
def error(self, exc):
|
||||
if not self.fut.done():
|
||||
self.fut.set_exception(exc)
|
||||
@@ -49,7 +67,7 @@ class PatternWaiter(asyncio.Protocol):
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
|
||||
|
||||
def data_received(self, data):
|
||||
spawn = self.expecter.spawn
|
||||
s = spawn._decoder.decode(data)
|
||||
@@ -67,7 +85,7 @@ class PatternWaiter(asyncio.Protocol):
|
||||
except Exception as e:
|
||||
self.expecter.errored()
|
||||
self.error(e)
|
||||
|
||||
|
||||
def eof_received(self):
|
||||
# N.B. If this gets called, async will close the pipe (the spawn object)
|
||||
# for us
|
||||
@@ -78,7 +96,7 @@ class PatternWaiter(asyncio.Protocol):
|
||||
self.error(e)
|
||||
else:
|
||||
self.found(index)
|
||||
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if isinstance(exc, OSError) and exc.errno == errno.EIO:
|
||||
# We may get here without eof_received being called, e.g on Linux
|
||||
|
||||
Vendored
+1
-1
@@ -244,7 +244,7 @@ class searcher_re(object):
|
||||
self.eof_index = -1
|
||||
self.timeout_index = -1
|
||||
self._searches = []
|
||||
for n, s in zip(list(range(len(patterns))), patterns):
|
||||
for n, s in enumerate(patterns):
|
||||
if s is EOF:
|
||||
self.eof_index = n
|
||||
continue
|
||||
|
||||
Vendored
+62
-40
@@ -430,61 +430,83 @@ class spawn(SpawnBase):
|
||||
available right away then one character will be returned immediately.
|
||||
It will not wait for 30 seconds for another 99 characters to come in.
|
||||
|
||||
This is a wrapper around os.read(). It uses select.select() to
|
||||
implement the timeout. '''
|
||||
On the other hand, if there are bytes available to read immediately,
|
||||
all those bytes will be read (up to the buffer size). So, if the
|
||||
buffer size is 1 megabyte and there is 1 megabyte of data available
|
||||
to read, the buffer will be filled, regardless of timeout.
|
||||
|
||||
This is a wrapper around os.read(). It uses select.select() or
|
||||
select.poll() to implement the timeout. '''
|
||||
|
||||
if self.closed:
|
||||
raise ValueError('I/O operation on closed file.')
|
||||
|
||||
if self.use_poll:
|
||||
def select(timeout):
|
||||
return poll_ignore_interrupts([self.child_fd], timeout)
|
||||
else:
|
||||
def select(timeout):
|
||||
return select_ignore_interrupts([self.child_fd], [], [], timeout)[0]
|
||||
|
||||
# If there is data available to read right now, read as much as
|
||||
# we can. We do this to increase performance if there are a lot
|
||||
# of bytes to be read. This also avoids calling isalive() too
|
||||
# often. See also:
|
||||
# * https://github.com/pexpect/pexpect/pull/304
|
||||
# * http://trac.sagemath.org/ticket/10295
|
||||
if select(0):
|
||||
try:
|
||||
incoming = super(spawn, self).read_nonblocking(size)
|
||||
except EOF:
|
||||
# Maybe the child is dead: update some attributes in that case
|
||||
self.isalive()
|
||||
raise
|
||||
while len(incoming) < size and select(0):
|
||||
try:
|
||||
incoming += super(spawn, self).read_nonblocking(size - len(incoming))
|
||||
except EOF:
|
||||
# Maybe the child is dead: update some attributes in that case
|
||||
self.isalive()
|
||||
# Don't raise EOF, just return what we read so far.
|
||||
return incoming
|
||||
return incoming
|
||||
|
||||
if timeout == -1:
|
||||
timeout = self.timeout
|
||||
|
||||
# Note that some systems such as Solaris do not give an EOF when
|
||||
# the child dies. In fact, you can still try to read
|
||||
# from the child_fd -- it will block forever or until TIMEOUT.
|
||||
# For this case, I test isalive() before doing any reading.
|
||||
# If isalive() is false, then I pretend that this is the same as EOF.
|
||||
if not self.isalive():
|
||||
# timeout of 0 means "poll"
|
||||
if self.use_poll:
|
||||
r = poll_ignore_interrupts([self.child_fd], timeout)
|
||||
else:
|
||||
r, w, e = select_ignore_interrupts([self.child_fd], [], [], 0)
|
||||
if not r:
|
||||
self.flag_eof = True
|
||||
raise EOF('End Of File (EOF). Braindead platform.')
|
||||
# The process is dead, but there may or may not be data
|
||||
# available to read. Note that some systems such as Solaris
|
||||
# do not give an EOF when the child dies. In fact, you can
|
||||
# still try to read from the child_fd -- it will block
|
||||
# forever or until TIMEOUT. For that reason, it's important
|
||||
# to do this check before calling select() with timeout.
|
||||
if select(0):
|
||||
return super(spawn, self).read_nonblocking(size)
|
||||
self.flag_eof = True
|
||||
raise EOF('End Of File (EOF). Braindead platform.')
|
||||
elif self.__irix_hack:
|
||||
# Irix takes a long time before it realizes a child was terminated.
|
||||
# Make sure that the timeout is at least 2 seconds.
|
||||
# FIXME So does this mean Irix systems are forced to always have
|
||||
# FIXME a 2 second delay when calling read_nonblocking? That sucks.
|
||||
if self.use_poll:
|
||||
r = poll_ignore_interrupts([self.child_fd], timeout)
|
||||
else:
|
||||
r, w, e = select_ignore_interrupts([self.child_fd], [], [], 2)
|
||||
if not r and not self.isalive():
|
||||
self.flag_eof = True
|
||||
raise EOF('End Of File (EOF). Slow platform.')
|
||||
if self.use_poll:
|
||||
r = poll_ignore_interrupts([self.child_fd], timeout)
|
||||
else:
|
||||
r, w, e = select_ignore_interrupts(
|
||||
[self.child_fd], [], [], timeout
|
||||
)
|
||||
if timeout is not None and timeout < 2:
|
||||
timeout = 2
|
||||
|
||||
if not r:
|
||||
if not self.isalive():
|
||||
# Some platforms, such as Irix, will claim that their
|
||||
# processes are alive; timeout on the select; and
|
||||
# then finally admit that they are not alive.
|
||||
self.flag_eof = True
|
||||
raise EOF('End of File (EOF). Very slow platform.')
|
||||
else:
|
||||
raise TIMEOUT('Timeout exceeded.')
|
||||
|
||||
if self.child_fd in r:
|
||||
# Because of the select(0) check above, we know that no data
|
||||
# is available right now. But if a non-zero timeout is given
|
||||
# (possibly timeout=None), we call select() with a timeout.
|
||||
if (timeout != 0) and select(timeout):
|
||||
return super(spawn, self).read_nonblocking(size)
|
||||
|
||||
raise ExceptionPexpect('Reached an unexpected state.') # pragma: no cover
|
||||
if not self.isalive():
|
||||
# Some platforms, such as Irix, will claim that their
|
||||
# processes are alive; timeout on the select; and
|
||||
# then finally admit that they are not alive.
|
||||
self.flag_eof = True
|
||||
raise EOF('End of File (EOF). Very slow platform.')
|
||||
else:
|
||||
raise TIMEOUT('Timeout exceeded.')
|
||||
|
||||
def write(self, s):
|
||||
'''This is similar to send() except that there is no return value.
|
||||
|
||||
Vendored
+58
-20
@@ -109,7 +109,7 @@ class pxssh (spawn):
|
||||
username = raw_input('username: ')
|
||||
password = getpass.getpass('password: ')
|
||||
s.login (hostname, username, password)
|
||||
|
||||
|
||||
`debug_command_string` is only for the test suite to confirm that the string
|
||||
generated for SSH is correct, using this will not allow you to do
|
||||
anything other than get a string back from `pxssh.pxssh.login()`.
|
||||
@@ -118,12 +118,12 @@ class pxssh (spawn):
|
||||
def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None,
|
||||
logfile=None, cwd=None, env=None, ignore_sighup=True, echo=True,
|
||||
options={}, encoding=None, codec_errors='strict',
|
||||
debug_command_string=False):
|
||||
debug_command_string=False, use_poll=False):
|
||||
|
||||
spawn.__init__(self, None, timeout=timeout, maxread=maxread,
|
||||
searchwindowsize=searchwindowsize, logfile=logfile,
|
||||
cwd=cwd, env=env, ignore_sighup=ignore_sighup, echo=echo,
|
||||
encoding=encoding, codec_errors=codec_errors)
|
||||
encoding=encoding, codec_errors=codec_errors, use_poll=use_poll)
|
||||
|
||||
self.name = '<pxssh>'
|
||||
|
||||
@@ -154,7 +154,7 @@ class pxssh (spawn):
|
||||
# Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying!
|
||||
#self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'"
|
||||
self.force_password = False
|
||||
|
||||
|
||||
self.debug_command_string = debug_command_string
|
||||
|
||||
# User defined SSH options, eg,
|
||||
@@ -220,7 +220,7 @@ class pxssh (spawn):
|
||||
can take 12 seconds. Low latency connections are more likely to fail
|
||||
with a low sync_multiplier. Best case sync time gets worse with a
|
||||
high sync multiplier (500 ms with default). '''
|
||||
|
||||
|
||||
# All of these timing pace values are magic.
|
||||
# I came up with these based on what seemed reliable for
|
||||
# connecting to a heavily loaded machine I have.
|
||||
@@ -253,20 +253,19 @@ class pxssh (spawn):
|
||||
### TODO: This is getting messy and I'm pretty sure this isn't perfect.
|
||||
### TODO: I need to draw a flow chart for this.
|
||||
### TODO: Unit tests for SSH tunnels, remote SSH command exec, disabling original prompt sync
|
||||
def login (self, server, username, password='', terminal_type='ansi',
|
||||
def login (self, server, username=None, password='', terminal_type='ansi',
|
||||
original_prompt=r"[#$]", login_timeout=10, port=None,
|
||||
auto_prompt_reset=True, ssh_key=None, quiet=True,
|
||||
sync_multiplier=1, check_local_ip=True,
|
||||
password_regex=r'(?i)(?:password:)|(?:passphrase for key)',
|
||||
ssh_tunnels={}, spawn_local_ssh=True,
|
||||
sync_original_prompt=True, ssh_config=None):
|
||||
sync_original_prompt=True, ssh_config=None, cmd='ssh'):
|
||||
'''This logs the user into the given server.
|
||||
|
||||
It uses
|
||||
'original_prompt' to try to find the prompt right after login. When it
|
||||
finds the prompt it immediately tries to reset the prompt to something
|
||||
more easily matched. The default 'original_prompt' is very optimistic
|
||||
and is easily fooled. It's more reliable to try to match the original
|
||||
It uses 'original_prompt' to try to find the prompt right after login.
|
||||
When it finds the prompt it immediately tries to reset the prompt to
|
||||
something more easily matched. The default 'original_prompt' is very
|
||||
optimistic and is easily fooled. It's more reliable to try to match the original
|
||||
prompt as exactly as possible to prevent false matches by server
|
||||
strings such as the "Message Of The Day". On many systems you can
|
||||
disable the MOTD on the remote server by creating a zero-length file
|
||||
@@ -284,27 +283,31 @@ class pxssh (spawn):
|
||||
uses a unique prompt in the :meth:`prompt` method. If the original prompt is
|
||||
not reset then this will disable the :meth:`prompt` method unless you
|
||||
manually set the :attr:`PROMPT` attribute.
|
||||
|
||||
|
||||
Set ``password_regex`` if there is a MOTD message with `password` in it.
|
||||
Changing this is like playing in traffic, don't (p)expect it to match straight
|
||||
away.
|
||||
|
||||
|
||||
If you require to connect to another SSH server from the your original SSH
|
||||
connection set ``spawn_local_ssh`` to `False` and this will use your current
|
||||
session to do so. Setting this option to `False` and not having an active session
|
||||
will trigger an error.
|
||||
|
||||
|
||||
Set ``ssh_key`` to a file path to an SSH private key to use that SSH key
|
||||
for the session authentication.
|
||||
Set ``ssh_key`` to `True` to force passing the current SSH authentication socket
|
||||
to the desired ``hostname``.
|
||||
|
||||
|
||||
Set ``ssh_config`` to a file path string of an SSH client config file to pass that
|
||||
file to the client to handle itself. You may set any options you wish in here, however
|
||||
doing so will require you to post extra information that you may not want to if you
|
||||
run into issues.
|
||||
|
||||
Alter the ``cmd`` to change the ssh client used, or to prepend it with network
|
||||
namespaces. For example ```cmd="ip netns exec vlan2 ssh"``` to execute the ssh in
|
||||
network namespace named ```vlan```.
|
||||
'''
|
||||
|
||||
|
||||
session_regex_array = ["(?i)are you sure you want to continue connecting", original_prompt, password_regex, "(?i)permission denied", "(?i)terminal type", TIMEOUT]
|
||||
session_init_regex_array = []
|
||||
session_init_regex_array.extend(session_regex_array)
|
||||
@@ -320,7 +323,7 @@ class pxssh (spawn):
|
||||
if ssh_config is not None:
|
||||
if spawn_local_ssh and not os.path.isfile(ssh_config):
|
||||
raise ExceptionPxssh('SSH config does not exist or is not a file.')
|
||||
ssh_options = ssh_options + '-F ' + ssh_config
|
||||
ssh_options = ssh_options + ' -F ' + ssh_config
|
||||
if port is not None:
|
||||
ssh_options = ssh_options + ' -p %s'%(str(port))
|
||||
if ssh_key is not None:
|
||||
@@ -331,7 +334,7 @@ class pxssh (spawn):
|
||||
if spawn_local_ssh and not os.path.isfile(ssh_key):
|
||||
raise ExceptionPxssh('private ssh key does not exist or is not a file.')
|
||||
ssh_options = ssh_options + ' -i %s' % (ssh_key)
|
||||
|
||||
|
||||
# SSH tunnels, make sure you know what you're putting into the lists
|
||||
# under each heading. Do not expect these to open 100% of the time,
|
||||
# The port you're requesting might be bound.
|
||||
@@ -354,7 +357,42 @@ class pxssh (spawn):
|
||||
if spawn_local_ssh==False:
|
||||
tunnel = quote(str(tunnel))
|
||||
ssh_options = ssh_options + ' -' + cmd_type + ' ' + str(tunnel)
|
||||
cmd = "ssh %s -l %s %s" % (ssh_options, username, server)
|
||||
|
||||
if username is not None:
|
||||
ssh_options = ssh_options + ' -l ' + username
|
||||
elif ssh_config is None:
|
||||
raise TypeError('login() needs either a username or an ssh_config')
|
||||
else: # make sure ssh_config has an entry for the server with a username
|
||||
with open(ssh_config, 'rt') as f:
|
||||
lines = [l.strip() for l in f.readlines()]
|
||||
|
||||
server_regex = r'^Host\s+%s\s*$' % server
|
||||
user_regex = r'^User\s+\w+\s*$'
|
||||
config_has_server = False
|
||||
server_has_username = False
|
||||
for line in lines:
|
||||
if not config_has_server and re.match(server_regex, line, re.IGNORECASE):
|
||||
config_has_server = True
|
||||
elif config_has_server and 'hostname' in line.lower():
|
||||
pass
|
||||
elif config_has_server and 'host' in line.lower():
|
||||
server_has_username = False # insurance
|
||||
break # we have left the relevant section
|
||||
elif config_has_server and re.match(user_regex, line, re.IGNORECASE):
|
||||
server_has_username = True
|
||||
break
|
||||
|
||||
if lines:
|
||||
del line
|
||||
|
||||
del lines
|
||||
|
||||
if not config_has_server:
|
||||
raise TypeError('login() ssh_config has no Host entry for %s' % server)
|
||||
elif not server_has_username:
|
||||
raise TypeError('login() ssh_config has no user entry for %s' % server)
|
||||
|
||||
cmd += " %s %s" % (ssh_options, server)
|
||||
if self.debug_command_string:
|
||||
return(cmd)
|
||||
|
||||
|
||||
Vendored
+11
-3
@@ -61,11 +61,11 @@ class REPLWrapper(object):
|
||||
self.child.expect(orig_prompt)
|
||||
self.child.sendline(prompt_change)
|
||||
|
||||
def _expect_prompt(self, timeout=-1):
|
||||
def _expect_prompt(self, timeout=-1, async_=False):
|
||||
return self.child.expect_exact([self.prompt, self.continuation_prompt],
|
||||
timeout=timeout)
|
||||
timeout=timeout, async_=async_)
|
||||
|
||||
def run_command(self, command, timeout=-1):
|
||||
def run_command(self, command, timeout=-1, async_=False):
|
||||
"""Send a command to the REPL, wait for and return output.
|
||||
|
||||
:param str command: The command to send. Trailing newlines are not needed.
|
||||
@@ -75,6 +75,10 @@ class REPLWrapper(object):
|
||||
:param int timeout: How long to wait for the next prompt. -1 means the
|
||||
default from the :class:`pexpect.spawn` object (default 30 seconds).
|
||||
None means to wait indefinitely.
|
||||
:param bool async_: On Python 3.4, or Python 3.3 with asyncio
|
||||
installed, passing ``async_=True`` will make this return an
|
||||
:mod:`asyncio` Future, which you can yield from to get the same
|
||||
result that this method would normally give directly.
|
||||
"""
|
||||
# Split up multiline commands and feed them in bit-by-bit
|
||||
cmdlines = command.splitlines()
|
||||
@@ -84,6 +88,10 @@ class REPLWrapper(object):
|
||||
if not cmdlines:
|
||||
raise ValueError("No command was given")
|
||||
|
||||
if async_:
|
||||
from ._async import repl_run_command_async
|
||||
return repl_run_command_async(self, cmdlines, timeout)
|
||||
|
||||
res = []
|
||||
self.child.sendline(cmdlines[0])
|
||||
for line in cmdlines[1:]:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Steve Dower
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -172,7 +172,7 @@ class RegistryAccessor(object):
|
||||
items = info.items()
|
||||
else:
|
||||
raise TypeError('info must be a dictionary')
|
||||
|
||||
|
||||
self._set_all_values(self._root, self.subkey, items, errors)
|
||||
if len(errors) == 1:
|
||||
raise ValueError(errors[0])
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
-e git+https://github.com/zooba/pep514tools.git@320e48745660b696e2dcaee888fc2e516b435e48#egg=pep514tools
|
||||
git+https://github.com/zooba/pep514tools.git@master#egg=pep514tools
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ from .models.lockfile import Lockfile
|
||||
from .models.pipfile import Pipfile
|
||||
from .models.requirements import Requirement
|
||||
|
||||
__version__ = "1.4.3"
|
||||
__version__ = "1.5.0"
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ def strip_ssh_from_git_uri(uri):
|
||||
|
||||
def add_ssh_scheme_to_git_uri(uri):
|
||||
# type: (S) -> S
|
||||
"""Cleans VCS uris from pip format"""
|
||||
"""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:
|
||||
|
||||
Vendored
+2
-2
@@ -37,7 +37,7 @@ if _scandir is None and ctypes is None:
|
||||
warnings.warn("scandir can't find the compiled _scandir C module "
|
||||
"or ctypes, using slow generic fallback")
|
||||
|
||||
__version__ = '1.9.0'
|
||||
__version__ = '1.10.0'
|
||||
__all__ = ['scandir', 'walk']
|
||||
|
||||
# Windows FILE_ATTRIBUTE constants for interpreting the
|
||||
@@ -583,7 +583,7 @@ elif sys.platform.startswith(('linux', 'darwin', 'sunos5')) or 'bsd' in sys.plat
|
||||
if _scandir is not None:
|
||||
scandir = scandir_c
|
||||
DirEntry = DirEntry_c
|
||||
elif ctypes is not None:
|
||||
elif ctypes is not None and have_dirent_d_type:
|
||||
scandir = scandir_python
|
||||
DirEntry = PosixDirEntry
|
||||
else:
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import os
|
||||
from ._core import ShellDetectionFailure
|
||||
|
||||
|
||||
__version__ = '1.2.8'
|
||||
__version__ = '1.3.1'
|
||||
|
||||
|
||||
def detect_shell(pid=None, max_depth=6):
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
SHELL_NAMES = {
|
||||
'sh', 'bash', 'dash', # Bourne.
|
||||
'sh', 'bash', 'dash', 'ash', # Bourne.
|
||||
'csh', 'tcsh', # C.
|
||||
'ksh', 'zsh', 'fish', # Common alternatives.
|
||||
'cmd', 'powershell', 'pwsh', # Microsoft.
|
||||
|
||||
Vendored
+1
-1
@@ -21,7 +21,7 @@ def _get_process_mapping():
|
||||
processes = {}
|
||||
for line in output.split('\n'):
|
||||
try:
|
||||
pid, ppid, args = line.strip().split(maxsplit=2)
|
||||
pid, ppid, args = line.strip().split(None, 2)
|
||||
except ValueError:
|
||||
continue
|
||||
processes[pid] = Process(
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import collections
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
Process = collections.namedtuple('Process', 'args pid ppid')
|
||||
|
||||
|
||||
def get_process_mapping():
|
||||
"""Try to look up the process tree via the output of `ps`.
|
||||
"""
|
||||
output = subprocess.check_output([
|
||||
'ps', '-ww', '-o', 'pid=', '-o', 'ppid=', '-o', 'args=',
|
||||
])
|
||||
if not isinstance(output, str):
|
||||
output = output.decode(sys.stdout.encoding)
|
||||
processes = {}
|
||||
for line in output.split('\n'):
|
||||
try:
|
||||
pid, ppid, args = line.strip().split(None, 2)
|
||||
except ValueError:
|
||||
continue
|
||||
processes[pid] = Process(
|
||||
args=tuple(shlex.split(args)), pid=pid, ppid=ppid,
|
||||
)
|
||||
return processes
|
||||
+14
-20
@@ -1,40 +1,34 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from ._core import Process
|
||||
from ._default import Process
|
||||
|
||||
|
||||
STAT_PPID = 3
|
||||
STAT_TTY = 6
|
||||
|
||||
STAT_PATTERN = re.compile(r'\(.+\)|\S+')
|
||||
|
||||
|
||||
def _get_stat(pid):
|
||||
with open(os.path.join('/proc', str(pid), 'stat')) as f:
|
||||
parts = STAT_PATTERN.findall(f.read())
|
||||
return parts[STAT_TTY], parts[STAT_PPID]
|
||||
|
||||
|
||||
def _get_cmdline(pid):
|
||||
with open(os.path.join('/proc', str(pid), 'cmdline')) as f:
|
||||
return tuple(f.read().split('\0')[:-1])
|
||||
|
||||
|
||||
def get_process_mapping():
|
||||
"""Try to look up the process tree via the /proc interface.
|
||||
"""
|
||||
self_tty = _get_stat(os.getpid())[0]
|
||||
with open('/proc/{0}/stat'.format(os.getpid())) as f:
|
||||
self_tty = f.read().split()[STAT_TTY]
|
||||
processes = {}
|
||||
for pid in os.listdir('/proc'):
|
||||
if not pid.isdigit():
|
||||
continue
|
||||
try:
|
||||
tty, ppid = _get_stat(pid)
|
||||
if tty != self_tty:
|
||||
continue
|
||||
args = _get_cmdline(pid)
|
||||
processes[pid] = Process(args=args, pid=pid, ppid=ppid)
|
||||
stat = '/proc/{0}/stat'.format(pid)
|
||||
cmdline = '/proc/{0}/cmdline'.format(pid)
|
||||
with open(stat) as fstat, open(cmdline) as fcmdline:
|
||||
stat = re.findall(r'\(.+\)|\S+', fstat.read())
|
||||
cmd = fcmdline.read().split('\x00')[:-1]
|
||||
ppid = stat[STAT_PPID]
|
||||
tty = stat[STAT_TTY]
|
||||
if tty == self_tty:
|
||||
processes[pid] = Process(
|
||||
args=tuple(cmd), pid=pid, ppid=ppid,
|
||||
)
|
||||
except IOError:
|
||||
# Process has disappeared - just ignore it.
|
||||
continue
|
||||
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
import collections
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ._core import Process
|
||||
|
||||
Process = collections.namedtuple('Process', 'args pid ppid')
|
||||
|
||||
|
||||
def get_process_mapping():
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from ._default import Process
|
||||
|
||||
|
||||
STAT_PPID = 3
|
||||
STAT_TTY = 6
|
||||
|
||||
|
||||
def get_process_mapping():
|
||||
"""Try to look up the process tree via Linux's /proc
|
||||
"""
|
||||
with open('/proc/{0}/stat'.format(os.getpid())) as f:
|
||||
self_tty = f.read().split()[STAT_TTY]
|
||||
processes = {}
|
||||
for pid in os.listdir('/proc'):
|
||||
if not pid.isdigit():
|
||||
continue
|
||||
try:
|
||||
stat = '/proc/{0}/stat'.format(pid)
|
||||
cmdline = '/proc/{0}/cmdline'.format(pid)
|
||||
with open(stat) as fstat, open(cmdline) as fcmdline:
|
||||
stat = re.findall(r'\(.+\)|\S+', fstat.read())
|
||||
cmd = fcmdline.read().split('\x00')[:-1]
|
||||
ppid = stat[STAT_PPID]
|
||||
tty = stat[STAT_TTY]
|
||||
if tty == self_tty:
|
||||
processes[pid] = Process(
|
||||
args=tuple(cmd), pid=pid, ppid=ppid,
|
||||
)
|
||||
except IOError:
|
||||
# Process has disappeared - just ignore it.
|
||||
continue
|
||||
return processes
|
||||
Vendored
+17
-15
@@ -1,19 +1,21 @@
|
||||
This is the MIT license: http://www.opensource.org/licenses/mit-license.php
|
||||
MIT License
|
||||
|
||||
Copyright 2008-2016 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
|
||||
Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Vendored
+1
-2
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
urllib3 - Thread-safe connection pooling and re-using.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import warnings
|
||||
|
||||
@@ -27,7 +26,7 @@ from logging import NullHandler
|
||||
|
||||
__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
|
||||
__license__ = 'MIT'
|
||||
__version__ = '1.24.1'
|
||||
__version__ = '1.25.2'
|
||||
|
||||
__all__ = (
|
||||
'HTTPConnectionPool',
|
||||
|
||||
Vendored
+22
-16
@@ -19,10 +19,11 @@ except (ImportError, AttributeError): # Platform-specific: No SSL.
|
||||
pass
|
||||
|
||||
|
||||
try: # Python 3:
|
||||
# Not a no-op, we're adding this to the namespace so it can be imported.
|
||||
try:
|
||||
# Python 3: not a no-op, we're adding this to the namespace so it can be imported.
|
||||
ConnectionError = ConnectionError
|
||||
except NameError: # Python 2:
|
||||
except NameError:
|
||||
# Python 2
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
@@ -101,7 +102,7 @@ class HTTPConnection(_HTTPConnection, object):
|
||||
is_verified = False
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
if six.PY3: # Python 3
|
||||
if six.PY3:
|
||||
kw.pop('strict', None)
|
||||
|
||||
# Pre-set source_address.
|
||||
@@ -158,7 +159,7 @@ class HTTPConnection(_HTTPConnection, object):
|
||||
conn = connection.create_connection(
|
||||
(self._dns_host, self.port), self.timeout, **extra_kw)
|
||||
|
||||
except SocketTimeout as e:
|
||||
except SocketTimeout:
|
||||
raise ConnectTimeoutError(
|
||||
self, "Connection to %s timed out. (connect timeout=%s)" %
|
||||
(self.host, self.timeout))
|
||||
@@ -171,7 +172,8 @@ class HTTPConnection(_HTTPConnection, object):
|
||||
|
||||
def _prepare_conn(self, conn):
|
||||
self.sock = conn
|
||||
if self._tunnel_host:
|
||||
# Google App Engine's httplib does not define _tunnel_host
|
||||
if getattr(self, '_tunnel_host', None):
|
||||
# TODO: Fix tunnel so it doesn't depend on self.sock state.
|
||||
self._tunnel()
|
||||
# Mark this connection as not reusable
|
||||
@@ -226,7 +228,8 @@ class HTTPSConnection(HTTPConnection):
|
||||
ssl_version = None
|
||||
|
||||
def __init__(self, host, port=None, key_file=None, cert_file=None,
|
||||
strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
key_password=None, strict=None,
|
||||
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
ssl_context=None, server_hostname=None, **kw):
|
||||
|
||||
HTTPConnection.__init__(self, host, port, strict=strict,
|
||||
@@ -234,6 +237,7 @@ class HTTPSConnection(HTTPConnection):
|
||||
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
self.key_password = key_password
|
||||
self.ssl_context = ssl_context
|
||||
self.server_hostname = server_hostname
|
||||
|
||||
@@ -255,6 +259,7 @@ class HTTPSConnection(HTTPConnection):
|
||||
sock=conn,
|
||||
keyfile=self.key_file,
|
||||
certfile=self.cert_file,
|
||||
key_password=self.key_password,
|
||||
ssl_context=self.ssl_context,
|
||||
server_hostname=self.server_hostname
|
||||
)
|
||||
@@ -272,25 +277,24 @@ class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
assert_fingerprint = None
|
||||
|
||||
def set_cert(self, key_file=None, cert_file=None,
|
||||
cert_reqs=None, ca_certs=None,
|
||||
cert_reqs=None, key_password=None, ca_certs=None,
|
||||
assert_hostname=None, assert_fingerprint=None,
|
||||
ca_cert_dir=None):
|
||||
"""
|
||||
This method should only be called once, before the connection is used.
|
||||
"""
|
||||
# If cert_reqs is not provided, we can try to guess. If the user gave
|
||||
# us a cert database, we assume they want to use it: otherwise, if
|
||||
# they gave us an SSL Context object we should use whatever is set for
|
||||
# it.
|
||||
# If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also
|
||||
# have an SSLContext object in which case we'll use its verify_mode.
|
||||
if cert_reqs is None:
|
||||
if ca_certs or ca_cert_dir:
|
||||
cert_reqs = 'CERT_REQUIRED'
|
||||
elif self.ssl_context is not None:
|
||||
if self.ssl_context is not None:
|
||||
cert_reqs = self.ssl_context.verify_mode
|
||||
else:
|
||||
cert_reqs = resolve_cert_reqs(None)
|
||||
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
self.cert_reqs = cert_reqs
|
||||
self.key_password = key_password
|
||||
self.assert_hostname = assert_hostname
|
||||
self.assert_fingerprint = assert_fingerprint
|
||||
self.ca_certs = ca_certs and os.path.expanduser(ca_certs)
|
||||
@@ -301,7 +305,8 @@ class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
conn = self._new_conn()
|
||||
hostname = self.host
|
||||
|
||||
if self._tunnel_host:
|
||||
# Google App Engine's httplib does not define _tunnel_host
|
||||
if getattr(self, '_tunnel_host', None):
|
||||
self.sock = conn
|
||||
# Calls self._set_hostport(), so self.host is
|
||||
# self._tunnel_host below.
|
||||
@@ -338,6 +343,7 @@ class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
sock=conn,
|
||||
keyfile=self.key_file,
|
||||
certfile=self.cert_file,
|
||||
key_password=self.key_password,
|
||||
ca_certs=self.ca_certs,
|
||||
ca_cert_dir=self.ca_cert_dir,
|
||||
server_hostname=server_hostname,
|
||||
|
||||
+21
-20
@@ -26,6 +26,7 @@ from .exceptions import (
|
||||
from .packages.ssl_match_hostname import CertificateError
|
||||
from .packages import six
|
||||
from .packages.six.moves import queue
|
||||
from .packages.rfc3986.normalizers import normalize_host
|
||||
from .connection import (
|
||||
port_by_scheme,
|
||||
DummyConnection,
|
||||
@@ -65,7 +66,7 @@ class ConnectionPool(object):
|
||||
if not host:
|
||||
raise LocationValueError("No host specified.")
|
||||
|
||||
self.host = _ipv6_host(host, self.scheme)
|
||||
self.host = _normalize_host(host, scheme=self.scheme)
|
||||
self._proxy_host = host.lower()
|
||||
self.port = port
|
||||
|
||||
@@ -373,9 +374,11 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
# Receive the response from the server
|
||||
try:
|
||||
try: # Python 2.7, use buffering of HTTP responses
|
||||
try:
|
||||
# Python 2.7, use buffering of HTTP responses
|
||||
httplib_response = conn.getresponse(buffering=True)
|
||||
except TypeError: # Python 3
|
||||
except TypeError:
|
||||
# Python 3
|
||||
try:
|
||||
httplib_response = conn.getresponse()
|
||||
except Exception as e:
|
||||
@@ -432,8 +435,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
|
||||
# TODO: Add optional support for socket.gethostbyname checking.
|
||||
scheme, host, port = get_host(url)
|
||||
|
||||
host = _ipv6_host(host, self.scheme)
|
||||
if host is not None:
|
||||
host = _normalize_host(host, scheme=scheme)
|
||||
|
||||
# Use explicit default port for comparison when none is given
|
||||
if self.port and not port:
|
||||
@@ -672,7 +675,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
# released back to the pool once the entire response is read
|
||||
response.read()
|
||||
except (TimeoutError, HTTPException, SocketError, ProtocolError,
|
||||
BaseSSLError, SSLError) as e:
|
||||
BaseSSLError, SSLError):
|
||||
pass
|
||||
|
||||
# Handle redirect?
|
||||
@@ -746,8 +749,8 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
If ``assert_hostname`` is False, no verification is done.
|
||||
|
||||
The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``,
|
||||
``ca_cert_dir``, and ``ssl_version`` are only used if :mod:`ssl` is
|
||||
available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade
|
||||
``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl`
|
||||
is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade
|
||||
the connection socket into an SSL socket.
|
||||
"""
|
||||
|
||||
@@ -759,7 +762,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
block=False, headers=None, retries=None,
|
||||
_proxy=None, _proxy_headers=None,
|
||||
key_file=None, cert_file=None, cert_reqs=None,
|
||||
ca_certs=None, ssl_version=None,
|
||||
key_password=None, ca_certs=None, ssl_version=None,
|
||||
assert_hostname=None, assert_fingerprint=None,
|
||||
ca_cert_dir=None, **conn_kw):
|
||||
|
||||
@@ -767,12 +770,10 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
block, headers, retries, _proxy, _proxy_headers,
|
||||
**conn_kw)
|
||||
|
||||
if ca_certs and cert_reqs is None:
|
||||
cert_reqs = 'CERT_REQUIRED'
|
||||
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
self.cert_reqs = cert_reqs
|
||||
self.key_password = key_password
|
||||
self.ca_certs = ca_certs
|
||||
self.ca_cert_dir = ca_cert_dir
|
||||
self.ssl_version = ssl_version
|
||||
@@ -787,6 +788,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
|
||||
if isinstance(conn, VerifiedHTTPSConnection):
|
||||
conn.set_cert(key_file=self.key_file,
|
||||
key_password=self.key_password,
|
||||
cert_file=self.cert_file,
|
||||
cert_reqs=self.cert_reqs,
|
||||
ca_certs=self.ca_certs,
|
||||
@@ -824,7 +826,9 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
|
||||
conn = self.ConnectionCls(host=actual_host, port=actual_port,
|
||||
timeout=self.timeout.connect_timeout,
|
||||
strict=self.strict, **self.conn_kw)
|
||||
strict=self.strict, cert_file=self.cert_file,
|
||||
key_file=self.key_file, key_password=self.key_password,
|
||||
**self.conn_kw)
|
||||
|
||||
return self._prepare_conn(conn)
|
||||
|
||||
@@ -875,9 +879,9 @@ def connection_from_url(url, **kw):
|
||||
return HTTPConnectionPool(host, port=port, **kw)
|
||||
|
||||
|
||||
def _ipv6_host(host, scheme):
|
||||
def _normalize_host(host, scheme):
|
||||
"""
|
||||
Process IPv6 address literals
|
||||
Normalize hosts for comparisons and use with sockets.
|
||||
"""
|
||||
|
||||
# httplib doesn't like it when we include brackets in IPv6 addresses
|
||||
@@ -886,11 +890,8 @@ def _ipv6_host(host, scheme):
|
||||
# Instead, we need to make sure we never pass ``None`` as the port.
|
||||
# However, for backward compatibility reasons we can't actually
|
||||
# *assert* that. See http://bugs.python.org/issue28539
|
||||
#
|
||||
# Also if an IPv6 address literal has a zone identifier, the
|
||||
# percent sign might be URIencoded, convert it back into ASCII
|
||||
if host.startswith('[') and host.endswith(']'):
|
||||
host = host.replace('%25', '%').strip('[]')
|
||||
host = host.strip('[]')
|
||||
if scheme in NORMALIZABLE_SCHEMES:
|
||||
host = host.lower()
|
||||
host = normalize_host(host)
|
||||
return host
|
||||
|
||||
@@ -516,6 +516,8 @@ class SecurityConst(object):
|
||||
kTLSProtocol1 = 4
|
||||
kTLSProtocol11 = 7
|
||||
kTLSProtocol12 = 8
|
||||
kTLSProtocol13 = 10
|
||||
kTLSProtocolMaxSupported = 999
|
||||
|
||||
kSSLClientSide = 1
|
||||
kSSLStreamType = 0
|
||||
@@ -558,30 +560,27 @@ class SecurityConst(object):
|
||||
errSecInvalidTrustSettings = -25262
|
||||
|
||||
# Cipher suites. We only pick the ones our default cipher string allows.
|
||||
# Source: https://developer.apple.com/documentation/security/1550981-ssl_cipher_suite_values
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C
|
||||
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xC030
|
||||
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B
|
||||
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F
|
||||
TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 = 0x00A3
|
||||
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA9
|
||||
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA8
|
||||
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = 0x009F
|
||||
TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 = 0x00A2
|
||||
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = 0x009E
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xC024
|
||||
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xC028
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xC00A
|
||||
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 0xC014
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 0x006B
|
||||
TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 = 0x006A
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x0039
|
||||
TLS_DHE_DSS_WITH_AES_256_CBC_SHA = 0x0038
|
||||
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xC023
|
||||
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xC027
|
||||
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xC009
|
||||
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xC013
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 0x0067
|
||||
TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 = 0x0040
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x0033
|
||||
TLS_DHE_DSS_WITH_AES_128_CBC_SHA = 0x0032
|
||||
TLS_RSA_WITH_AES_256_GCM_SHA384 = 0x009D
|
||||
TLS_RSA_WITH_AES_128_GCM_SHA256 = 0x009C
|
||||
TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x003D
|
||||
@@ -590,4 +589,5 @@ class SecurityConst(object):
|
||||
TLS_RSA_WITH_AES_128_CBC_SHA = 0x002F
|
||||
TLS_AES_128_GCM_SHA256 = 0x1301
|
||||
TLS_AES_256_GCM_SHA384 = 0x1302
|
||||
TLS_CHACHA20_POLY1305_SHA256 = 0x1303
|
||||
TLS_AES_128_CCM_8_SHA256 = 0x1305
|
||||
TLS_AES_128_CCM_SHA256 = 0x1304
|
||||
|
||||
+27
-8
@@ -70,6 +70,7 @@ import sys
|
||||
|
||||
from .. import util
|
||||
|
||||
|
||||
__all__ = ['inject_into_urllib3', 'extract_from_urllib3']
|
||||
|
||||
# SNI always works.
|
||||
@@ -77,20 +78,19 @@ HAS_SNI = True
|
||||
|
||||
# Map from urllib3 to PyOpenSSL compatible parameter-values.
|
||||
_openssl_versions = {
|
||||
ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
|
||||
util.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD,
|
||||
ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
|
||||
}
|
||||
|
||||
if hasattr(ssl, 'PROTOCOL_SSLv3') and hasattr(OpenSSL.SSL, 'SSLv3_METHOD'):
|
||||
_openssl_versions[ssl.PROTOCOL_SSLv3] = OpenSSL.SSL.SSLv3_METHOD
|
||||
|
||||
if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'):
|
||||
_openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD
|
||||
|
||||
if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'):
|
||||
_openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD
|
||||
|
||||
try:
|
||||
_openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD})
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
_stdlib_to_openssl_verify = {
|
||||
ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
|
||||
@@ -117,6 +117,7 @@ def inject_into_urllib3():
|
||||
|
||||
_validate_dependencies_met()
|
||||
|
||||
util.SSLContext = PyOpenSSLContext
|
||||
util.ssl_.SSLContext = PyOpenSSLContext
|
||||
util.HAS_SNI = HAS_SNI
|
||||
util.ssl_.HAS_SNI = HAS_SNI
|
||||
@@ -127,6 +128,7 @@ def inject_into_urllib3():
|
||||
def extract_from_urllib3():
|
||||
'Undo monkey-patching by :func:`inject_into_urllib3`.'
|
||||
|
||||
util.SSLContext = orig_util_SSLContext
|
||||
util.ssl_.SSLContext = orig_util_SSLContext
|
||||
util.HAS_SNI = orig_util_HAS_SNI
|
||||
util.ssl_.HAS_SNI = orig_util_HAS_SNI
|
||||
@@ -184,6 +186,10 @@ def _dnsname_to_stdlib(name):
|
||||
except idna.core.IDNAError:
|
||||
return None
|
||||
|
||||
# Don't send IPv6 addresses through the IDNA encoder.
|
||||
if ':' in name:
|
||||
return name
|
||||
|
||||
name = idna_encode(name)
|
||||
if name is None:
|
||||
return None
|
||||
@@ -276,7 +282,7 @@ class WrappedSocket(object):
|
||||
return b''
|
||||
else:
|
||||
raise SocketError(str(e))
|
||||
except OpenSSL.SSL.ZeroReturnError as e:
|
||||
except OpenSSL.SSL.ZeroReturnError:
|
||||
if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
|
||||
return b''
|
||||
else:
|
||||
@@ -286,6 +292,10 @@ class WrappedSocket(object):
|
||||
raise timeout('The read operation timed out')
|
||||
else:
|
||||
return self.recv(*args, **kwargs)
|
||||
|
||||
# TLS 1.3 post-handshake authentication
|
||||
except OpenSSL.SSL.Error as e:
|
||||
raise ssl.SSLError("read error: %r" % e)
|
||||
else:
|
||||
return data
|
||||
|
||||
@@ -297,7 +307,7 @@ class WrappedSocket(object):
|
||||
return 0
|
||||
else:
|
||||
raise SocketError(str(e))
|
||||
except OpenSSL.SSL.ZeroReturnError as e:
|
||||
except OpenSSL.SSL.ZeroReturnError:
|
||||
if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
|
||||
return 0
|
||||
else:
|
||||
@@ -308,6 +318,10 @@ class WrappedSocket(object):
|
||||
else:
|
||||
return self.recv_into(*args, **kwargs)
|
||||
|
||||
# TLS 1.3 post-handshake authentication
|
||||
except OpenSSL.SSL.Error as e:
|
||||
raise ssl.SSLError("read error: %r" % e)
|
||||
|
||||
def settimeout(self, timeout):
|
||||
return self.socket.settimeout(timeout)
|
||||
|
||||
@@ -360,6 +374,9 @@ class WrappedSocket(object):
|
||||
'subjectAltName': get_subj_alt_name(x509)
|
||||
}
|
||||
|
||||
def version(self):
|
||||
return self.connection.get_protocol_version_name()
|
||||
|
||||
def _reuse(self):
|
||||
self._makefile_refs += 1
|
||||
|
||||
@@ -432,7 +449,9 @@ class PyOpenSSLContext(object):
|
||||
def load_cert_chain(self, certfile, keyfile=None, password=None):
|
||||
self._ctx.use_certificate_chain_file(certfile)
|
||||
if password is not None:
|
||||
self._ctx.set_passwd_cb(lambda max_length, prompt_twice, userdata: password)
|
||||
if not isinstance(password, six.binary_type):
|
||||
password = password.encode('utf-8')
|
||||
self._ctx.set_passwd_cb(lambda *_: password)
|
||||
self._ctx.use_privatekey_file(keyfile or certfile)
|
||||
|
||||
def wrap_socket(self, sock, server_side=False,
|
||||
|
||||
+68
-19
@@ -23,6 +23,31 @@ To use this module, simply import and inject it::
|
||||
urllib3.contrib.securetransport.inject_into_urllib3()
|
||||
|
||||
Happy TLSing!
|
||||
|
||||
This code is a bastardised version of the code found in Will Bond's oscrypto
|
||||
library. An enormous debt is owed to him for blazing this trail for us. For
|
||||
that reason, this code should be considered to be covered both by urllib3's
|
||||
license and by oscrypto's:
|
||||
|
||||
Copyright (c) 2015-2016 Will Bond <will@wbond.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
@@ -86,35 +111,32 @@ SSL_WRITE_BLOCKSIZE = 16384
|
||||
# individual cipher suites. We need to do this because this is how
|
||||
# SecureTransport wants them.
|
||||
CIPHER_SUITES = [
|
||||
SecurityConst.TLS_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_CHACHA20_POLY1305_SHA256,
|
||||
SecurityConst.TLS_AES_128_GCM_SHA256,
|
||||
SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
SecurityConst.TLS_DHE_DSS_WITH_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_DHE_DSS_WITH_AES_128_GCM_SHA256,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
|
||||
SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,
|
||||
SecurityConst.TLS_DHE_DSS_WITH_AES_256_CBC_SHA256,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
SecurityConst.TLS_DHE_DSS_WITH_AES_256_CBC_SHA,
|
||||
SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
SecurityConst.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
SecurityConst.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
SecurityConst.TLS_DHE_DSS_WITH_AES_128_CBC_SHA256,
|
||||
SecurityConst.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
SecurityConst.TLS_DHE_DSS_WITH_AES_128_CBC_SHA,
|
||||
SecurityConst.TLS_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_AES_128_GCM_SHA256,
|
||||
SecurityConst.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
SecurityConst.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
SecurityConst.TLS_AES_128_CCM_8_SHA256,
|
||||
SecurityConst.TLS_AES_128_CCM_SHA256,
|
||||
SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA256,
|
||||
SecurityConst.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
||||
SecurityConst.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
@@ -122,9 +144,10 @@ CIPHER_SUITES = [
|
||||
]
|
||||
|
||||
# Basically this is simple: for PROTOCOL_SSLv23 we turn it into a low of
|
||||
# TLSv1 and a high of TLSv1.2. For everything else, we pin to that version.
|
||||
# TLSv1 and a high of TLSv1.3. For everything else, we pin to that version.
|
||||
# TLSv1 to 1.2 are supported on macOS 10.8+ and TLSv1.3 is macOS 10.13+
|
||||
_protocol_to_min_max = {
|
||||
ssl.PROTOCOL_SSLv23: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12),
|
||||
util.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocolMaxSupported),
|
||||
}
|
||||
|
||||
if hasattr(ssl, "PROTOCOL_SSLv2"):
|
||||
@@ -147,14 +170,13 @@ if hasattr(ssl, "PROTOCOL_TLSv1_2"):
|
||||
_protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = (
|
||||
SecurityConst.kTLSProtocol12, SecurityConst.kTLSProtocol12
|
||||
)
|
||||
if hasattr(ssl, "PROTOCOL_TLS"):
|
||||
_protocol_to_min_max[ssl.PROTOCOL_TLS] = _protocol_to_min_max[ssl.PROTOCOL_SSLv23]
|
||||
|
||||
|
||||
def inject_into_urllib3():
|
||||
"""
|
||||
Monkey-patch urllib3 with SecureTransport-backed SSL-support.
|
||||
"""
|
||||
util.SSLContext = SecureTransportContext
|
||||
util.ssl_.SSLContext = SecureTransportContext
|
||||
util.HAS_SNI = HAS_SNI
|
||||
util.ssl_.HAS_SNI = HAS_SNI
|
||||
@@ -166,6 +188,7 @@ def extract_from_urllib3():
|
||||
"""
|
||||
Undo monkey-patching by :func:`inject_into_urllib3`.
|
||||
"""
|
||||
util.SSLContext = orig_util_SSLContext
|
||||
util.ssl_.SSLContext = orig_util_SSLContext
|
||||
util.HAS_SNI = orig_util_HAS_SNI
|
||||
util.ssl_.HAS_SNI = orig_util_HAS_SNI
|
||||
@@ -458,7 +481,14 @@ class WrappedSocket(object):
|
||||
# Set the minimum and maximum TLS versions.
|
||||
result = Security.SSLSetProtocolVersionMin(self.context, min_version)
|
||||
_assert_no_error(result)
|
||||
|
||||
# TLS 1.3 isn't necessarily enabled by the OS
|
||||
# so we have to detect when we error out and try
|
||||
# setting TLS 1.3 if it's allowed. kTLSProtocolMaxSupported
|
||||
# was added in macOS 10.13 along with kTLSProtocol13.
|
||||
result = Security.SSLSetProtocolVersionMax(self.context, max_version)
|
||||
if result != 0 and max_version == SecurityConst.kTLSProtocolMaxSupported:
|
||||
result = Security.SSLSetProtocolVersionMax(self.context, SecurityConst.kTLSProtocol12)
|
||||
_assert_no_error(result)
|
||||
|
||||
# If there's a trust DB, we need to use it. We do that by telling
|
||||
@@ -667,6 +697,25 @@ class WrappedSocket(object):
|
||||
|
||||
return der_bytes
|
||||
|
||||
def version(self):
|
||||
protocol = Security.SSLProtocol()
|
||||
result = Security.SSLGetNegotiatedProtocolVersion(self.context, ctypes.byref(protocol))
|
||||
_assert_no_error(result)
|
||||
if protocol.value == SecurityConst.kTLSProtocol13:
|
||||
return 'TLSv1.3'
|
||||
elif protocol.value == SecurityConst.kTLSProtocol12:
|
||||
return 'TLSv1.2'
|
||||
elif protocol.value == SecurityConst.kTLSProtocol11:
|
||||
return 'TLSv1.1'
|
||||
elif protocol.value == SecurityConst.kTLSProtocol1:
|
||||
return 'TLSv1'
|
||||
elif protocol.value == SecurityConst.kSSLProtocol3:
|
||||
return 'SSLv3'
|
||||
elif protocol.value == SecurityConst.kSSLProtocol2:
|
||||
return 'SSLv2'
|
||||
else:
|
||||
raise ssl.SSLError('Unknown TLS version: %r' % protocol)
|
||||
|
||||
def _reuse(self):
|
||||
self._makefile_refs += 1
|
||||
|
||||
|
||||
+24
-11
@@ -1,25 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module contains provisional support for SOCKS proxies from within
|
||||
urllib3. This module supports SOCKS4 (specifically the SOCKS4A variant) and
|
||||
urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and
|
||||
SOCKS5. To enable its functionality, either install PySocks or install this
|
||||
module with the ``socks`` extra.
|
||||
|
||||
The SOCKS implementation supports the full range of urllib3 features. It also
|
||||
supports the following SOCKS features:
|
||||
|
||||
- SOCKS4
|
||||
- SOCKS4a
|
||||
- SOCKS5
|
||||
- SOCKS4A (``proxy_url='socks4a://...``)
|
||||
- SOCKS4 (``proxy_url='socks4://...``)
|
||||
- SOCKS5 with remote DNS (``proxy_url='socks5h://...``)
|
||||
- SOCKS5 with local DNS (``proxy_url='socks5://...``)
|
||||
- Usernames and passwords for the SOCKS proxy
|
||||
|
||||
Known Limitations:
|
||||
.. note::
|
||||
It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in
|
||||
your ``proxy_url`` to ensure that DNS resolution is done from the remote
|
||||
server instead of client-side when connecting to a domain name.
|
||||
|
||||
SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5
|
||||
supports IPv4, IPv6, and domain names.
|
||||
|
||||
When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url``
|
||||
will be sent as the ``userid`` section of the SOCKS request::
|
||||
|
||||
proxy_url="socks4a://<userid>@proxy-host"
|
||||
|
||||
When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion
|
||||
of the ``proxy_url`` will be sent as the username/password to authenticate
|
||||
with the proxy::
|
||||
|
||||
proxy_url="socks5h://<username>:<password>@proxy-host"
|
||||
|
||||
- Currently PySocks does not support contacting remote websites via literal
|
||||
IPv6 addresses. Any such connection attempt will fail. You must use a domain
|
||||
name.
|
||||
- Currently PySocks does not support IPv6 connections to the SOCKS proxy. Any
|
||||
such connection attempt will fail.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
@@ -88,7 +101,7 @@ class SOCKSConnection(HTTPConnection):
|
||||
**extra_kw
|
||||
)
|
||||
|
||||
except SocketTimeout as e:
|
||||
except SocketTimeout:
|
||||
raise ConnectTimeoutError(
|
||||
self, "Connection to %s timed out. (connect timeout=%s)" %
|
||||
(self.host, self.timeout))
|
||||
|
||||
Vendored
+117
-23
@@ -1,6 +1,7 @@
|
||||
from __future__ import absolute_import
|
||||
import email.utils
|
||||
import mimetypes
|
||||
import re
|
||||
|
||||
from .packages import six
|
||||
|
||||
@@ -19,57 +20,147 @@ def guess_content_type(filename, default='application/octet-stream'):
|
||||
return default
|
||||
|
||||
|
||||
def format_header_param(name, value):
|
||||
def format_header_param_rfc2231(name, value):
|
||||
"""
|
||||
Helper function to format and quote a single header parameter.
|
||||
Helper function to format and quote a single header parameter using the
|
||||
strategy defined in RFC 2231.
|
||||
|
||||
Particularly useful for header parameters which might contain
|
||||
non-ASCII values, like file names. This follows RFC 2231, as
|
||||
suggested by RFC 2388 Section 4.4.
|
||||
non-ASCII values, like file names. This follows RFC 2388 Section 4.4.
|
||||
|
||||
:param name:
|
||||
The name of the parameter, a string expected to be ASCII only.
|
||||
:param value:
|
||||
The value of the parameter, provided as a unicode string.
|
||||
The value of the parameter, provided as ``bytes`` or `str``.
|
||||
:ret:
|
||||
An RFC-2231-formatted unicode string.
|
||||
"""
|
||||
if isinstance(value, six.binary_type):
|
||||
value = value.decode("utf-8")
|
||||
|
||||
if not any(ch in value for ch in '"\\\r\n'):
|
||||
result = '%s="%s"' % (name, value)
|
||||
result = u'%s="%s"' % (name, value)
|
||||
try:
|
||||
result.encode('ascii')
|
||||
except (UnicodeEncodeError, UnicodeDecodeError):
|
||||
pass
|
||||
else:
|
||||
return result
|
||||
if not six.PY3 and isinstance(value, six.text_type): # Python 2:
|
||||
|
||||
if not six.PY3: # Python 2:
|
||||
value = value.encode('utf-8')
|
||||
|
||||
# encode_rfc2231 accepts an encoded string and returns an ascii-encoded
|
||||
# string in Python 2 but accepts and returns unicode strings in Python 3
|
||||
value = email.utils.encode_rfc2231(value, 'utf-8')
|
||||
value = '%s*=%s' % (name, value)
|
||||
|
||||
if not six.PY3: # Python 2:
|
||||
value = value.decode('utf-8')
|
||||
|
||||
return value
|
||||
|
||||
|
||||
_HTML5_REPLACEMENTS = {
|
||||
u"\u0022": u"%22",
|
||||
# Replace "\" with "\\".
|
||||
u"\u005C": u"\u005C\u005C",
|
||||
u"\u005C": u"\u005C\u005C",
|
||||
}
|
||||
|
||||
# All control characters from 0x00 to 0x1F *except* 0x1B.
|
||||
_HTML5_REPLACEMENTS.update({
|
||||
six.unichr(cc): u"%{:02X}".format(cc)
|
||||
for cc
|
||||
in range(0x00, 0x1F+1)
|
||||
if cc not in (0x1B,)
|
||||
})
|
||||
|
||||
|
||||
def _replace_multiple(value, needles_and_replacements):
|
||||
|
||||
def replacer(match):
|
||||
return needles_and_replacements[match.group(0)]
|
||||
|
||||
pattern = re.compile(
|
||||
r"|".join([
|
||||
re.escape(needle) for needle in needles_and_replacements.keys()
|
||||
])
|
||||
)
|
||||
|
||||
result = pattern.sub(replacer, value)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def format_header_param_html5(name, value):
|
||||
"""
|
||||
Helper function to format and quote a single header parameter using the
|
||||
HTML5 strategy.
|
||||
|
||||
Particularly useful for header parameters which might contain
|
||||
non-ASCII values, like file names. This follows the `HTML5 Working Draft
|
||||
Section 4.10.22.7`_ and matches the behavior of curl and modern browsers.
|
||||
|
||||
.. _HTML5 Working Draft Section 4.10.22.7:
|
||||
https://w3c.github.io/html/sec-forms.html#multipart-form-data
|
||||
|
||||
:param name:
|
||||
The name of the parameter, a string expected to be ASCII only.
|
||||
:param value:
|
||||
The value of the parameter, provided as ``bytes`` or `str``.
|
||||
:ret:
|
||||
A unicode string, stripped of troublesome characters.
|
||||
"""
|
||||
if isinstance(value, six.binary_type):
|
||||
value = value.decode("utf-8")
|
||||
|
||||
value = _replace_multiple(value, _HTML5_REPLACEMENTS)
|
||||
|
||||
return u'%s="%s"' % (name, value)
|
||||
|
||||
|
||||
# For backwards-compatibility.
|
||||
format_header_param = format_header_param_html5
|
||||
|
||||
|
||||
class RequestField(object):
|
||||
"""
|
||||
A data container for request body parameters.
|
||||
|
||||
:param name:
|
||||
The name of this request field.
|
||||
The name of this request field. Must be unicode.
|
||||
:param data:
|
||||
The data/value body.
|
||||
:param filename:
|
||||
An optional filename of the request field.
|
||||
An optional filename of the request field. Must be unicode.
|
||||
:param headers:
|
||||
An optional dict-like object of headers to initially use for the field.
|
||||
:param header_formatter:
|
||||
An optional callable that is used to encode and format the headers. By
|
||||
default, this is :func:`format_header_param_html5`.
|
||||
"""
|
||||
def __init__(self, name, data, filename=None, headers=None):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
data,
|
||||
filename=None,
|
||||
headers=None,
|
||||
header_formatter=format_header_param_html5):
|
||||
self._name = name
|
||||
self._filename = filename
|
||||
self.data = data
|
||||
self.headers = {}
|
||||
if headers:
|
||||
self.headers = dict(headers)
|
||||
self.header_formatter = header_formatter
|
||||
|
||||
@classmethod
|
||||
def from_tuples(cls, fieldname, value):
|
||||
def from_tuples(
|
||||
cls,
|
||||
fieldname,
|
||||
value,
|
||||
header_formatter=format_header_param_html5):
|
||||
"""
|
||||
A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters.
|
||||
|
||||
@@ -97,21 +188,24 @@ class RequestField(object):
|
||||
content_type = None
|
||||
data = value
|
||||
|
||||
request_param = cls(fieldname, data, filename=filename)
|
||||
request_param = cls(
|
||||
fieldname, data, filename=filename, header_formatter=header_formatter)
|
||||
request_param.make_multipart(content_type=content_type)
|
||||
|
||||
return request_param
|
||||
|
||||
def _render_part(self, name, value):
|
||||
"""
|
||||
Overridable helper function to format a single header parameter.
|
||||
Overridable helper function to format a single header parameter. By
|
||||
default, this calls ``self.header_formatter``.
|
||||
|
||||
:param name:
|
||||
The name of the parameter, a string expected to be ASCII only.
|
||||
:param value:
|
||||
The value of the parameter, provided as a unicode string.
|
||||
"""
|
||||
return format_header_param(name, value)
|
||||
|
||||
return self.header_formatter(name, value)
|
||||
|
||||
def _render_parts(self, header_parts):
|
||||
"""
|
||||
@@ -133,7 +227,7 @@ class RequestField(object):
|
||||
if value is not None:
|
||||
parts.append(self._render_part(name, value))
|
||||
|
||||
return '; '.join(parts)
|
||||
return u'; '.join(parts)
|
||||
|
||||
def render_headers(self):
|
||||
"""
|
||||
@@ -144,15 +238,15 @@ class RequestField(object):
|
||||
sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location']
|
||||
for sort_key in sort_keys:
|
||||
if self.headers.get(sort_key, False):
|
||||
lines.append('%s: %s' % (sort_key, self.headers[sort_key]))
|
||||
lines.append(u'%s: %s' % (sort_key, self.headers[sort_key]))
|
||||
|
||||
for header_name, header_value in self.headers.items():
|
||||
if header_name not in sort_keys:
|
||||
if header_value:
|
||||
lines.append('%s: %s' % (header_name, header_value))
|
||||
lines.append(u'%s: %s' % (header_name, header_value))
|
||||
|
||||
lines.append('\r\n')
|
||||
return '\r\n'.join(lines)
|
||||
lines.append(u'\r\n')
|
||||
return u'\r\n'.join(lines)
|
||||
|
||||
def make_multipart(self, content_disposition=None, content_type=None,
|
||||
content_location=None):
|
||||
@@ -168,10 +262,10 @@ class RequestField(object):
|
||||
The 'Content-Location' of the request body.
|
||||
|
||||
"""
|
||||
self.headers['Content-Disposition'] = content_disposition or 'form-data'
|
||||
self.headers['Content-Disposition'] += '; '.join([
|
||||
'', self._render_parts(
|
||||
(('name', self._name), ('filename', self._filename))
|
||||
self.headers['Content-Disposition'] = content_disposition or u'form-data'
|
||||
self.headers['Content-Disposition'] += u'; '.join([
|
||||
u'', self._render_parts(
|
||||
((u'name', self._name), (u'filename', self._filename))
|
||||
)
|
||||
])
|
||||
self.headers['Content-Type'] = content_type
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Rackspace
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
An implementation of semantics and validations described in RFC 3986.
|
||||
|
||||
See http://rfc3986.readthedocs.io/ for detailed documentation.
|
||||
|
||||
:copyright: (c) 2014 Rackspace
|
||||
:license: Apache v2.0, see LICENSE for details
|
||||
"""
|
||||
|
||||
from .api import iri_reference
|
||||
from .api import IRIReference
|
||||
from .api import is_valid_uri
|
||||
from .api import normalize_uri
|
||||
from .api import uri_reference
|
||||
from .api import URIReference
|
||||
from .api import urlparse
|
||||
from .parseresult import ParseResult
|
||||
|
||||
__title__ = 'rfc3986'
|
||||
__author__ = 'Ian Stapleton Cordasco'
|
||||
__author_email__ = 'graffatcolmingov@gmail.com'
|
||||
__license__ = 'Apache v2.0'
|
||||
__copyright__ = 'Copyright 2014 Rackspace'
|
||||
__version__ = '1.3.1'
|
||||
|
||||
__all__ = (
|
||||
'ParseResult',
|
||||
'URIReference',
|
||||
'IRIReference',
|
||||
'is_valid_uri',
|
||||
'normalize_uri',
|
||||
'uri_reference',
|
||||
'iri_reference',
|
||||
'urlparse',
|
||||
'__title__',
|
||||
'__author__',
|
||||
'__author_email__',
|
||||
'__license__',
|
||||
'__copyright__',
|
||||
'__version__',
|
||||
)
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
"""Module containing the implementation of the URIMixin class."""
|
||||
import warnings
|
||||
|
||||
from . import exceptions as exc
|
||||
from . import misc
|
||||
from . import normalizers
|
||||
from . import validators
|
||||
|
||||
|
||||
class URIMixin(object):
|
||||
"""Mixin with all shared methods for URIs and IRIs."""
|
||||
|
||||
__hash__ = tuple.__hash__
|
||||
|
||||
def authority_info(self):
|
||||
"""Return a dictionary with the ``userinfo``, ``host``, and ``port``.
|
||||
|
||||
If the authority is not valid, it will raise a
|
||||
:class:`~rfc3986.exceptions.InvalidAuthority` Exception.
|
||||
|
||||
:returns:
|
||||
``{'userinfo': 'username:password', 'host': 'www.example.com',
|
||||
'port': '80'}``
|
||||
:rtype: dict
|
||||
:raises rfc3986.exceptions.InvalidAuthority:
|
||||
If the authority is not ``None`` and can not be parsed.
|
||||
"""
|
||||
if not self.authority:
|
||||
return {'userinfo': None, 'host': None, 'port': None}
|
||||
|
||||
match = self._match_subauthority()
|
||||
|
||||
if match is None:
|
||||
# In this case, we have an authority that was parsed from the URI
|
||||
# Reference, but it cannot be further parsed by our
|
||||
# misc.SUBAUTHORITY_MATCHER. In this case it must not be a valid
|
||||
# authority.
|
||||
raise exc.InvalidAuthority(self.authority.encode(self.encoding))
|
||||
|
||||
# We had a match, now let's ensure that it is actually a valid host
|
||||
# address if it is IPv4
|
||||
matches = match.groupdict()
|
||||
host = matches.get('host')
|
||||
|
||||
if (host and misc.IPv4_MATCHER.match(host) and not
|
||||
validators.valid_ipv4_host_address(host)):
|
||||
# If we have a host, it appears to be IPv4 and it does not have
|
||||
# valid bytes, it is an InvalidAuthority.
|
||||
raise exc.InvalidAuthority(self.authority.encode(self.encoding))
|
||||
|
||||
return matches
|
||||
|
||||
def _match_subauthority(self):
|
||||
return misc.SUBAUTHORITY_MATCHER.match(self.authority)
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""If present, a string representing the host."""
|
||||
try:
|
||||
authority = self.authority_info()
|
||||
except exc.InvalidAuthority:
|
||||
return None
|
||||
return authority['host']
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""If present, the port extracted from the authority."""
|
||||
try:
|
||||
authority = self.authority_info()
|
||||
except exc.InvalidAuthority:
|
||||
return None
|
||||
return authority['port']
|
||||
|
||||
@property
|
||||
def userinfo(self):
|
||||
"""If present, the userinfo extracted from the authority."""
|
||||
try:
|
||||
authority = self.authority_info()
|
||||
except exc.InvalidAuthority:
|
||||
return None
|
||||
return authority['userinfo']
|
||||
|
||||
def is_absolute(self):
|
||||
"""Determine if this URI Reference is an absolute URI.
|
||||
|
||||
See http://tools.ietf.org/html/rfc3986#section-4.3 for explanation.
|
||||
|
||||
:returns: ``True`` if it is an absolute URI, ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(misc.ABSOLUTE_URI_MATCHER.match(self.unsplit()))
|
||||
|
||||
def is_valid(self, **kwargs):
|
||||
"""Determine if the URI is valid.
|
||||
|
||||
.. deprecated:: 1.1.0
|
||||
|
||||
Use the :class:`~rfc3986.validators.Validator` object instead.
|
||||
|
||||
:param bool require_scheme: Set to ``True`` if you wish to require the
|
||||
presence of the scheme component.
|
||||
:param bool require_authority: Set to ``True`` if you wish to require
|
||||
the presence of the authority component.
|
||||
:param bool require_path: Set to ``True`` if you wish to require the
|
||||
presence of the path component.
|
||||
:param bool require_query: Set to ``True`` if you wish to require the
|
||||
presence of the query component.
|
||||
:param bool require_fragment: Set to ``True`` if you wish to require
|
||||
the presence of the fragment component.
|
||||
:returns: ``True`` if the URI is valid. ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
warnings.warn("Please use rfc3986.validators.Validator instead. "
|
||||
"This method will be eventually removed.",
|
||||
DeprecationWarning)
|
||||
validators = [
|
||||
(self.scheme_is_valid, kwargs.get('require_scheme', False)),
|
||||
(self.authority_is_valid, kwargs.get('require_authority', False)),
|
||||
(self.path_is_valid, kwargs.get('require_path', False)),
|
||||
(self.query_is_valid, kwargs.get('require_query', False)),
|
||||
(self.fragment_is_valid, kwargs.get('require_fragment', False)),
|
||||
]
|
||||
return all(v(r) for v, r in validators)
|
||||
|
||||
def authority_is_valid(self, require=False):
|
||||
"""Determine if the authority component is valid.
|
||||
|
||||
.. deprecated:: 1.1.0
|
||||
|
||||
Use the :class:`~rfc3986.validators.Validator` object instead.
|
||||
|
||||
:param bool require:
|
||||
Set to ``True`` to require the presence of this component.
|
||||
:returns:
|
||||
``True`` if the authority is valid. ``False`` otherwise.
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
warnings.warn("Please use rfc3986.validators.Validator instead. "
|
||||
"This method will be eventually removed.",
|
||||
DeprecationWarning)
|
||||
try:
|
||||
self.authority_info()
|
||||
except exc.InvalidAuthority:
|
||||
return False
|
||||
|
||||
return validators.authority_is_valid(
|
||||
self.authority,
|
||||
host=self.host,
|
||||
require=require,
|
||||
)
|
||||
|
||||
def scheme_is_valid(self, require=False):
|
||||
"""Determine if the scheme component is valid.
|
||||
|
||||
.. deprecated:: 1.1.0
|
||||
|
||||
Use the :class:`~rfc3986.validators.Validator` object instead.
|
||||
|
||||
:param str require: Set to ``True`` to require the presence of this
|
||||
component.
|
||||
:returns: ``True`` if the scheme is valid. ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
warnings.warn("Please use rfc3986.validators.Validator instead. "
|
||||
"This method will be eventually removed.",
|
||||
DeprecationWarning)
|
||||
return validators.scheme_is_valid(self.scheme, require)
|
||||
|
||||
def path_is_valid(self, require=False):
|
||||
"""Determine if the path component is valid.
|
||||
|
||||
.. deprecated:: 1.1.0
|
||||
|
||||
Use the :class:`~rfc3986.validators.Validator` object instead.
|
||||
|
||||
:param str require: Set to ``True`` to require the presence of this
|
||||
component.
|
||||
:returns: ``True`` if the path is valid. ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
warnings.warn("Please use rfc3986.validators.Validator instead. "
|
||||
"This method will be eventually removed.",
|
||||
DeprecationWarning)
|
||||
return validators.path_is_valid(self.path, require)
|
||||
|
||||
def query_is_valid(self, require=False):
|
||||
"""Determine if the query component is valid.
|
||||
|
||||
.. deprecated:: 1.1.0
|
||||
|
||||
Use the :class:`~rfc3986.validators.Validator` object instead.
|
||||
|
||||
:param str require: Set to ``True`` to require the presence of this
|
||||
component.
|
||||
:returns: ``True`` if the query is valid. ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
warnings.warn("Please use rfc3986.validators.Validator instead. "
|
||||
"This method will be eventually removed.",
|
||||
DeprecationWarning)
|
||||
return validators.query_is_valid(self.query, require)
|
||||
|
||||
def fragment_is_valid(self, require=False):
|
||||
"""Determine if the fragment component is valid.
|
||||
|
||||
.. deprecated:: 1.1.0
|
||||
|
||||
Use the Validator object instead.
|
||||
|
||||
:param str require: Set to ``True`` to require the presence of this
|
||||
component.
|
||||
:returns: ``True`` if the fragment is valid. ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
warnings.warn("Please use rfc3986.validators.Validator instead. "
|
||||
"This method will be eventually removed.",
|
||||
DeprecationWarning)
|
||||
return validators.fragment_is_valid(self.fragment, require)
|
||||
|
||||
def normalized_equality(self, other_ref):
|
||||
"""Compare this URIReference to another URIReference.
|
||||
|
||||
:param URIReference other_ref: (required), The reference with which
|
||||
we're comparing.
|
||||
:returns: ``True`` if the references are equal, ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
return tuple(self.normalize()) == tuple(other_ref.normalize())
|
||||
|
||||
def resolve_with(self, base_uri, strict=False):
|
||||
"""Use an absolute URI Reference to resolve this relative reference.
|
||||
|
||||
Assuming this is a relative reference that you would like to resolve,
|
||||
use the provided base URI to resolve it.
|
||||
|
||||
See http://tools.ietf.org/html/rfc3986#section-5 for more information.
|
||||
|
||||
:param base_uri: Either a string or URIReference. It must be an
|
||||
absolute URI or it will raise an exception.
|
||||
:returns: A new URIReference which is the result of resolving this
|
||||
reference using ``base_uri``.
|
||||
:rtype: :class:`URIReference`
|
||||
:raises rfc3986.exceptions.ResolutionError:
|
||||
If the ``base_uri`` is not an absolute URI.
|
||||
"""
|
||||
if not isinstance(base_uri, URIMixin):
|
||||
base_uri = type(self).from_string(base_uri)
|
||||
|
||||
if not base_uri.is_absolute():
|
||||
raise exc.ResolutionError(base_uri)
|
||||
|
||||
# This is optional per
|
||||
# http://tools.ietf.org/html/rfc3986#section-5.2.1
|
||||
base_uri = base_uri.normalize()
|
||||
|
||||
# The reference we're resolving
|
||||
resolving = self
|
||||
|
||||
if not strict and resolving.scheme == base_uri.scheme:
|
||||
resolving = resolving.copy_with(scheme=None)
|
||||
|
||||
# http://tools.ietf.org/html/rfc3986#page-32
|
||||
if resolving.scheme is not None:
|
||||
target = resolving.copy_with(
|
||||
path=normalizers.normalize_path(resolving.path)
|
||||
)
|
||||
else:
|
||||
if resolving.authority is not None:
|
||||
target = resolving.copy_with(
|
||||
scheme=base_uri.scheme,
|
||||
path=normalizers.normalize_path(resolving.path)
|
||||
)
|
||||
else:
|
||||
if resolving.path is None:
|
||||
if resolving.query is not None:
|
||||
query = resolving.query
|
||||
else:
|
||||
query = base_uri.query
|
||||
target = resolving.copy_with(
|
||||
scheme=base_uri.scheme,
|
||||
authority=base_uri.authority,
|
||||
path=base_uri.path,
|
||||
query=query
|
||||
)
|
||||
else:
|
||||
if resolving.path.startswith('/'):
|
||||
path = normalizers.normalize_path(resolving.path)
|
||||
else:
|
||||
path = normalizers.normalize_path(
|
||||
misc.merge_paths(base_uri, resolving.path)
|
||||
)
|
||||
target = resolving.copy_with(
|
||||
scheme=base_uri.scheme,
|
||||
authority=base_uri.authority,
|
||||
path=path,
|
||||
query=resolving.query
|
||||
)
|
||||
return target
|
||||
|
||||
def unsplit(self):
|
||||
"""Create a URI string from the components.
|
||||
|
||||
:returns: The URI Reference reconstituted as a string.
|
||||
:rtype: str
|
||||
"""
|
||||
# See http://tools.ietf.org/html/rfc3986#section-5.3
|
||||
result_list = []
|
||||
if self.scheme:
|
||||
result_list.extend([self.scheme, ':'])
|
||||
if self.authority:
|
||||
result_list.extend(['//', self.authority])
|
||||
if self.path:
|
||||
result_list.append(self.path)
|
||||
if self.query is not None:
|
||||
result_list.extend(['?', self.query])
|
||||
if self.fragment is not None:
|
||||
result_list.extend(['#', self.fragment])
|
||||
return ''.join(result_list)
|
||||
|
||||
def copy_with(self, scheme=misc.UseExisting, authority=misc.UseExisting,
|
||||
path=misc.UseExisting, query=misc.UseExisting,
|
||||
fragment=misc.UseExisting):
|
||||
"""Create a copy of this reference with the new components.
|
||||
|
||||
:param str scheme:
|
||||
(optional) The scheme to use for the new reference.
|
||||
:param str authority:
|
||||
(optional) The authority to use for the new reference.
|
||||
:param str path:
|
||||
(optional) The path to use for the new reference.
|
||||
:param str query:
|
||||
(optional) The query to use for the new reference.
|
||||
:param str fragment:
|
||||
(optional) The fragment to use for the new reference.
|
||||
:returns:
|
||||
New URIReference with provided components.
|
||||
:rtype:
|
||||
URIReference
|
||||
"""
|
||||
attributes = {
|
||||
'scheme': scheme,
|
||||
'authority': authority,
|
||||
'path': path,
|
||||
'query': query,
|
||||
'fragment': fragment,
|
||||
}
|
||||
for key, value in list(attributes.items()):
|
||||
if value is misc.UseExisting:
|
||||
del attributes[key]
|
||||
uri = self._replace(**attributes)
|
||||
uri.encoding = self.encoding
|
||||
return uri
|
||||
@@ -0,0 +1,267 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Module for the regular expressions crafted from ABNF."""
|
||||
|
||||
import sys
|
||||
|
||||
# https://tools.ietf.org/html/rfc3986#page-13
|
||||
GEN_DELIMS = GENERIC_DELIMITERS = ":/?#[]@"
|
||||
GENERIC_DELIMITERS_SET = set(GENERIC_DELIMITERS)
|
||||
# https://tools.ietf.org/html/rfc3986#page-13
|
||||
SUB_DELIMS = SUB_DELIMITERS = "!$&'()*+,;="
|
||||
SUB_DELIMITERS_SET = set(SUB_DELIMITERS)
|
||||
# Escape the '*' for use in regular expressions
|
||||
SUB_DELIMITERS_RE = r"!$&'()\*+,;="
|
||||
RESERVED_CHARS_SET = GENERIC_DELIMITERS_SET.union(SUB_DELIMITERS_SET)
|
||||
ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
DIGIT = '0123456789'
|
||||
# https://tools.ietf.org/html/rfc3986#section-2.3
|
||||
UNRESERVED = UNRESERVED_CHARS = ALPHA + DIGIT + r'._!-'
|
||||
UNRESERVED_CHARS_SET = set(UNRESERVED_CHARS)
|
||||
NON_PCT_ENCODED_SET = RESERVED_CHARS_SET.union(UNRESERVED_CHARS_SET)
|
||||
# We need to escape the '-' in this case:
|
||||
UNRESERVED_RE = r'A-Za-z0-9._~\-'
|
||||
|
||||
# Percent encoded character values
|
||||
PERCENT_ENCODED = PCT_ENCODED = '%[A-Fa-f0-9]{2}'
|
||||
PCHAR = '([' + UNRESERVED_RE + SUB_DELIMITERS_RE + ':@]|%s)' % PCT_ENCODED
|
||||
|
||||
# NOTE(sigmavirus24): We're going to use more strict regular expressions
|
||||
# than appear in Appendix B for scheme. This will prevent over-eager
|
||||
# consuming of items that aren't schemes.
|
||||
SCHEME_RE = '[a-zA-Z][a-zA-Z0-9+.-]*'
|
||||
_AUTHORITY_RE = '[^/?#]*'
|
||||
_PATH_RE = '[^?#]*'
|
||||
_QUERY_RE = '[^#]*'
|
||||
_FRAGMENT_RE = '.*'
|
||||
|
||||
# Extracted from http://tools.ietf.org/html/rfc3986#appendix-B
|
||||
COMPONENT_PATTERN_DICT = {
|
||||
'scheme': SCHEME_RE,
|
||||
'authority': _AUTHORITY_RE,
|
||||
'path': _PATH_RE,
|
||||
'query': _QUERY_RE,
|
||||
'fragment': _FRAGMENT_RE,
|
||||
}
|
||||
|
||||
# See http://tools.ietf.org/html/rfc3986#appendix-B
|
||||
# In this case, we name each of the important matches so we can use
|
||||
# SRE_Match#groupdict to parse the values out if we so choose. This is also
|
||||
# modified to ignore other matches that are not important to the parsing of
|
||||
# the reference so we can also simply use SRE_Match#groups.
|
||||
URL_PARSING_RE = (
|
||||
r'(?:(?P<scheme>{scheme}):)?(?://(?P<authority>{authority}))?'
|
||||
r'(?P<path>{path})(?:\?(?P<query>{query}))?'
|
||||
r'(?:#(?P<fragment>{fragment}))?'
|
||||
).format(**COMPONENT_PATTERN_DICT)
|
||||
|
||||
|
||||
# #########################
|
||||
# Authority Matcher Section
|
||||
# #########################
|
||||
|
||||
# Host patterns, see: http://tools.ietf.org/html/rfc3986#section-3.2.2
|
||||
# The pattern for a regular name, e.g., www.google.com, api.github.com
|
||||
REGULAR_NAME_RE = REG_NAME = '((?:{0}|[{1}])*)'.format(
|
||||
'%[0-9A-Fa-f]{2}', SUB_DELIMITERS_RE + UNRESERVED_RE
|
||||
)
|
||||
# The pattern for an IPv4 address, e.g., 192.168.255.255, 127.0.0.1,
|
||||
IPv4_RE = r'([0-9]{1,3}\.){3}[0-9]{1,3}'
|
||||
# Hexadecimal characters used in each piece of an IPv6 address
|
||||
HEXDIG_RE = '[0-9A-Fa-f]{1,4}'
|
||||
# Least-significant 32 bits of an IPv6 address
|
||||
LS32_RE = '({hex}:{hex}|{ipv4})'.format(hex=HEXDIG_RE, ipv4=IPv4_RE)
|
||||
# Substitutions into the following patterns for IPv6 patterns defined
|
||||
# http://tools.ietf.org/html/rfc3986#page-20
|
||||
_subs = {'hex': HEXDIG_RE, 'ls32': LS32_RE}
|
||||
|
||||
# Below: h16 = hexdig, see: https://tools.ietf.org/html/rfc5234 for details
|
||||
# about ABNF (Augmented Backus-Naur Form) use in the comments
|
||||
variations = [
|
||||
# 6( h16 ":" ) ls32
|
||||
'(%(hex)s:){6}%(ls32)s' % _subs,
|
||||
# "::" 5( h16 ":" ) ls32
|
||||
'::(%(hex)s:){5}%(ls32)s' % _subs,
|
||||
# [ h16 ] "::" 4( h16 ":" ) ls32
|
||||
'(%(hex)s)?::(%(hex)s:){4}%(ls32)s' % _subs,
|
||||
# [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
|
||||
'((%(hex)s:)?%(hex)s)?::(%(hex)s:){3}%(ls32)s' % _subs,
|
||||
# [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
|
||||
'((%(hex)s:){0,2}%(hex)s)?::(%(hex)s:){2}%(ls32)s' % _subs,
|
||||
# [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
|
||||
'((%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s' % _subs,
|
||||
# [ *4( h16 ":" ) h16 ] "::" ls32
|
||||
'((%(hex)s:){0,4}%(hex)s)?::%(ls32)s' % _subs,
|
||||
# [ *5( h16 ":" ) h16 ] "::" h16
|
||||
'((%(hex)s:){0,5}%(hex)s)?::%(hex)s' % _subs,
|
||||
# [ *6( h16 ":" ) h16 ] "::"
|
||||
'((%(hex)s:){0,6}%(hex)s)?::' % _subs,
|
||||
]
|
||||
|
||||
IPv6_RE = '(({0})|({1})|({2})|({3})|({4})|({5})|({6})|({7})|({8}))'.format(
|
||||
*variations
|
||||
)
|
||||
|
||||
IPv_FUTURE_RE = r'v[0-9A-Fa-f]+\.[%s]+' % (
|
||||
UNRESERVED_RE + SUB_DELIMITERS_RE + ':'
|
||||
)
|
||||
|
||||
# RFC 6874 Zone ID ABNF
|
||||
ZONE_ID = '(?:[' + UNRESERVED_RE + ']|' + PCT_ENCODED + ')+'
|
||||
|
||||
IPv6_ADDRZ_RFC4007_RE = IPv6_RE + '(?:(?:%25|%)' + ZONE_ID + ')?'
|
||||
IPv6_ADDRZ_RE = IPv6_RE + '(?:%25' + ZONE_ID + ')?'
|
||||
|
||||
IP_LITERAL_RE = r'\[({0}|{1})\]'.format(
|
||||
IPv6_ADDRZ_RFC4007_RE,
|
||||
IPv_FUTURE_RE,
|
||||
)
|
||||
|
||||
# Pattern for matching the host piece of the authority
|
||||
HOST_RE = HOST_PATTERN = '({0}|{1}|{2})'.format(
|
||||
REG_NAME,
|
||||
IPv4_RE,
|
||||
IP_LITERAL_RE,
|
||||
)
|
||||
USERINFO_RE = '^([' + UNRESERVED_RE + SUB_DELIMITERS_RE + ':]|%s)+' % (
|
||||
PCT_ENCODED
|
||||
)
|
||||
PORT_RE = '[0-9]{1,5}'
|
||||
|
||||
# ####################
|
||||
# Path Matcher Section
|
||||
# ####################
|
||||
|
||||
# See http://tools.ietf.org/html/rfc3986#section-3.3 for more information
|
||||
# about the path patterns defined below.
|
||||
segments = {
|
||||
'segment': PCHAR + '*',
|
||||
# Non-zero length segment
|
||||
'segment-nz': PCHAR + '+',
|
||||
# Non-zero length segment without ":"
|
||||
'segment-nz-nc': PCHAR.replace(':', '') + '+'
|
||||
}
|
||||
|
||||
# Path types taken from Section 3.3 (linked above)
|
||||
PATH_EMPTY = '^$'
|
||||
PATH_ROOTLESS = '%(segment-nz)s(/%(segment)s)*' % segments
|
||||
PATH_NOSCHEME = '%(segment-nz-nc)s(/%(segment)s)*' % segments
|
||||
PATH_ABSOLUTE = '/(%s)?' % PATH_ROOTLESS
|
||||
PATH_ABEMPTY = '(/%(segment)s)*' % segments
|
||||
PATH_RE = '^(%s|%s|%s|%s|%s)$' % (
|
||||
PATH_ABEMPTY, PATH_ABSOLUTE, PATH_NOSCHEME, PATH_ROOTLESS, PATH_EMPTY
|
||||
)
|
||||
|
||||
FRAGMENT_RE = QUERY_RE = (
|
||||
'^([/?:@' + UNRESERVED_RE + SUB_DELIMITERS_RE + ']|%s)*$' % PCT_ENCODED
|
||||
)
|
||||
|
||||
# ##########################
|
||||
# Relative reference matcher
|
||||
# ##########################
|
||||
|
||||
# See http://tools.ietf.org/html/rfc3986#section-4.2 for details
|
||||
RELATIVE_PART_RE = '(//%s%s|%s|%s|%s)' % (
|
||||
COMPONENT_PATTERN_DICT['authority'],
|
||||
PATH_ABEMPTY,
|
||||
PATH_ABSOLUTE,
|
||||
PATH_NOSCHEME,
|
||||
PATH_EMPTY,
|
||||
)
|
||||
|
||||
# See http://tools.ietf.org/html/rfc3986#section-3 for definition
|
||||
HIER_PART_RE = '(//%s%s|%s|%s|%s)' % (
|
||||
COMPONENT_PATTERN_DICT['authority'],
|
||||
PATH_ABEMPTY,
|
||||
PATH_ABSOLUTE,
|
||||
PATH_ROOTLESS,
|
||||
PATH_EMPTY,
|
||||
)
|
||||
|
||||
# ###############
|
||||
# IRIs / RFC 3987
|
||||
# ###############
|
||||
|
||||
# Only wide-unicode gets the high-ranges of UCSCHAR
|
||||
if sys.maxunicode > 0xFFFF: # pragma: no cover
|
||||
IPRIVATE = u'\uE000-\uF8FF\U000F0000-\U000FFFFD\U00100000-\U0010FFFD'
|
||||
UCSCHAR_RE = (
|
||||
u'\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF'
|
||||
u'\U00010000-\U0001FFFD\U00020000-\U0002FFFD'
|
||||
u'\U00030000-\U0003FFFD\U00040000-\U0004FFFD'
|
||||
u'\U00050000-\U0005FFFD\U00060000-\U0006FFFD'
|
||||
u'\U00070000-\U0007FFFD\U00080000-\U0008FFFD'
|
||||
u'\U00090000-\U0009FFFD\U000A0000-\U000AFFFD'
|
||||
u'\U000B0000-\U000BFFFD\U000C0000-\U000CFFFD'
|
||||
u'\U000D0000-\U000DFFFD\U000E1000-\U000EFFFD'
|
||||
)
|
||||
else: # pragma: no cover
|
||||
IPRIVATE = u'\uE000-\uF8FF'
|
||||
UCSCHAR_RE = (
|
||||
u'\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF'
|
||||
)
|
||||
|
||||
IUNRESERVED_RE = u'A-Za-z0-9\\._~\\-' + UCSCHAR_RE
|
||||
IPCHAR = u'([' + IUNRESERVED_RE + SUB_DELIMITERS_RE + u':@]|%s)' % PCT_ENCODED
|
||||
|
||||
isegments = {
|
||||
'isegment': IPCHAR + u'*',
|
||||
# Non-zero length segment
|
||||
'isegment-nz': IPCHAR + u'+',
|
||||
# Non-zero length segment without ":"
|
||||
'isegment-nz-nc': IPCHAR.replace(':', '') + u'+'
|
||||
}
|
||||
|
||||
IPATH_ROOTLESS = u'%(isegment-nz)s(/%(isegment)s)*' % isegments
|
||||
IPATH_NOSCHEME = u'%(isegment-nz-nc)s(/%(isegment)s)*' % isegments
|
||||
IPATH_ABSOLUTE = u'/(?:%s)?' % IPATH_ROOTLESS
|
||||
IPATH_ABEMPTY = u'(?:/%(isegment)s)*' % isegments
|
||||
IPATH_RE = u'^(?:%s|%s|%s|%s|%s)$' % (
|
||||
IPATH_ABEMPTY, IPATH_ABSOLUTE, IPATH_NOSCHEME, IPATH_ROOTLESS, PATH_EMPTY
|
||||
)
|
||||
|
||||
IREGULAR_NAME_RE = IREG_NAME = u'(?:{0}|[{1}])*'.format(
|
||||
u'%[0-9A-Fa-f]{2}', SUB_DELIMITERS_RE + IUNRESERVED_RE
|
||||
)
|
||||
|
||||
IHOST_RE = IHOST_PATTERN = u'({0}|{1}|{2})'.format(
|
||||
IREG_NAME,
|
||||
IPv4_RE,
|
||||
IP_LITERAL_RE,
|
||||
)
|
||||
|
||||
IUSERINFO_RE = u'^(?:[' + IUNRESERVED_RE + SUB_DELIMITERS_RE + u':]|%s)+' % (
|
||||
PCT_ENCODED
|
||||
)
|
||||
|
||||
IFRAGMENT_RE = (u'^(?:[/?:@' + IUNRESERVED_RE + SUB_DELIMITERS_RE
|
||||
+ u']|%s)*$' % PCT_ENCODED)
|
||||
IQUERY_RE = (u'^(?:[/?:@' + IUNRESERVED_RE + SUB_DELIMITERS_RE
|
||||
+ IPRIVATE + u']|%s)*$' % PCT_ENCODED)
|
||||
|
||||
IRELATIVE_PART_RE = u'(//%s%s|%s|%s|%s)' % (
|
||||
COMPONENT_PATTERN_DICT['authority'],
|
||||
IPATH_ABEMPTY,
|
||||
IPATH_ABSOLUTE,
|
||||
IPATH_NOSCHEME,
|
||||
PATH_EMPTY,
|
||||
)
|
||||
|
||||
IHIER_PART_RE = u'(//%s%s|%s|%s|%s)' % (
|
||||
COMPONENT_PATTERN_DICT['authority'],
|
||||
IPATH_ABEMPTY,
|
||||
IPATH_ABSOLUTE,
|
||||
IPATH_ROOTLESS,
|
||||
PATH_EMPTY,
|
||||
)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Rackspace
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Module containing the simple and functional API for rfc3986.
|
||||
|
||||
This module defines functions and provides access to the public attributes
|
||||
and classes of rfc3986.
|
||||
"""
|
||||
|
||||
from .iri import IRIReference
|
||||
from .parseresult import ParseResult
|
||||
from .uri import URIReference
|
||||
|
||||
|
||||
def uri_reference(uri, encoding='utf-8'):
|
||||
"""Parse a URI string into a URIReference.
|
||||
|
||||
This is a convenience function. You could achieve the same end by using
|
||||
``URIReference.from_string(uri)``.
|
||||
|
||||
:param str uri: The URI which needs to be parsed into a reference.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:returns: A parsed URI
|
||||
:rtype: :class:`URIReference`
|
||||
"""
|
||||
return URIReference.from_string(uri, encoding)
|
||||
|
||||
|
||||
def iri_reference(iri, encoding='utf-8'):
|
||||
"""Parse a IRI string into an IRIReference.
|
||||
|
||||
This is a convenience function. You could achieve the same end by using
|
||||
``IRIReference.from_string(iri)``.
|
||||
|
||||
:param str iri: The IRI which needs to be parsed into a reference.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:returns: A parsed IRI
|
||||
:rtype: :class:`IRIReference`
|
||||
"""
|
||||
return IRIReference.from_string(iri, encoding)
|
||||
|
||||
|
||||
def is_valid_uri(uri, encoding='utf-8', **kwargs):
|
||||
"""Determine if the URI given is valid.
|
||||
|
||||
This is a convenience function. You could use either
|
||||
``uri_reference(uri).is_valid()`` or
|
||||
``URIReference.from_string(uri).is_valid()`` to achieve the same result.
|
||||
|
||||
:param str uri: The URI to be validated.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:param bool require_scheme: Set to ``True`` if you wish to require the
|
||||
presence of the scheme component.
|
||||
:param bool require_authority: Set to ``True`` if you wish to require the
|
||||
presence of the authority component.
|
||||
:param bool require_path: Set to ``True`` if you wish to require the
|
||||
presence of the path component.
|
||||
:param bool require_query: Set to ``True`` if you wish to require the
|
||||
presence of the query component.
|
||||
:param bool require_fragment: Set to ``True`` if you wish to require the
|
||||
presence of the fragment component.
|
||||
:returns: ``True`` if the URI is valid, ``False`` otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
return URIReference.from_string(uri, encoding).is_valid(**kwargs)
|
||||
|
||||
|
||||
def normalize_uri(uri, encoding='utf-8'):
|
||||
"""Normalize the given URI.
|
||||
|
||||
This is a convenience function. You could use either
|
||||
``uri_reference(uri).normalize().unsplit()`` or
|
||||
``URIReference.from_string(uri).normalize().unsplit()`` instead.
|
||||
|
||||
:param str uri: The URI to be normalized.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:returns: The normalized URI.
|
||||
:rtype: str
|
||||
"""
|
||||
normalized_reference = URIReference.from_string(uri, encoding).normalize()
|
||||
return normalized_reference.unsplit()
|
||||
|
||||
|
||||
def urlparse(uri, encoding='utf-8'):
|
||||
"""Parse a given URI and return a ParseResult.
|
||||
|
||||
This is a partial replacement of the standard library's urlparse function.
|
||||
|
||||
:param str uri: The URI to be parsed.
|
||||
:param str encoding: The encoding of the string provided.
|
||||
:returns: A parsed URI
|
||||
:rtype: :class:`~rfc3986.parseresult.ParseResult`
|
||||
"""
|
||||
return ParseResult.from_string(uri, encoding, strict=False)
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Ian Stapleton Cordasco
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Module containing the logic for the URIBuilder object."""
|
||||
from . import compat
|
||||
from . import normalizers
|
||||
from . import uri
|
||||
|
||||
|
||||
class URIBuilder(object):
|
||||
"""Object to aid in building up a URI Reference from parts.
|
||||
|
||||
.. note::
|
||||
|
||||
This object should be instantiated by the user, but it's recommended
|
||||
that it is not provided with arguments. Instead, use the available
|
||||
method to populate the fields.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, scheme=None, userinfo=None, host=None, port=None,
|
||||
path=None, query=None, fragment=None):
|
||||
"""Initialize our URI builder.
|
||||
|
||||
:param str scheme:
|
||||
(optional)
|
||||
:param str userinfo:
|
||||
(optional)
|
||||
:param str host:
|
||||
(optional)
|
||||
:param int port:
|
||||
(optional)
|
||||
:param str path:
|
||||
(optional)
|
||||
:param str query:
|
||||
(optional)
|
||||
:param str fragment:
|
||||
(optional)
|
||||
"""
|
||||
self.scheme = scheme
|
||||
self.userinfo = userinfo
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.path = path
|
||||
self.query = query
|
||||
self.fragment = fragment
|
||||
|
||||
def __repr__(self):
|
||||
"""Provide a convenient view of our builder object."""
|
||||
formatstr = ('URIBuilder(scheme={b.scheme}, userinfo={b.userinfo}, '
|
||||
'host={b.host}, port={b.port}, path={b.path}, '
|
||||
'query={b.query}, fragment={b.fragment})')
|
||||
return formatstr.format(b=self)
|
||||
|
||||
def add_scheme(self, scheme):
|
||||
"""Add a scheme to our builder object.
|
||||
|
||||
After normalizing, this will generate a new URIBuilder instance with
|
||||
the specified scheme and all other attributes the same.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_scheme('HTTPS')
|
||||
URIBuilder(scheme='https', userinfo=None, host=None, port=None,
|
||||
path=None, query=None, fragment=None)
|
||||
|
||||
"""
|
||||
scheme = normalizers.normalize_scheme(scheme)
|
||||
return URIBuilder(
|
||||
scheme=scheme,
|
||||
userinfo=self.userinfo,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
path=self.path,
|
||||
query=self.query,
|
||||
fragment=self.fragment,
|
||||
)
|
||||
|
||||
def add_credentials(self, username, password):
|
||||
"""Add credentials as the userinfo portion of the URI.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_credentials('root', 's3crete')
|
||||
URIBuilder(scheme=None, userinfo='root:s3crete', host=None,
|
||||
port=None, path=None, query=None, fragment=None)
|
||||
|
||||
>>> URIBuilder().add_credentials('root', None)
|
||||
URIBuilder(scheme=None, userinfo='root', host=None,
|
||||
port=None, path=None, query=None, fragment=None)
|
||||
"""
|
||||
if username is None:
|
||||
raise ValueError('Username cannot be None')
|
||||
userinfo = normalizers.normalize_username(username)
|
||||
|
||||
if password is not None:
|
||||
userinfo = '{}:{}'.format(
|
||||
userinfo,
|
||||
normalizers.normalize_password(password),
|
||||
)
|
||||
|
||||
return URIBuilder(
|
||||
scheme=self.scheme,
|
||||
userinfo=userinfo,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
path=self.path,
|
||||
query=self.query,
|
||||
fragment=self.fragment,
|
||||
)
|
||||
|
||||
def add_host(self, host):
|
||||
"""Add hostname to the URI.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_host('google.com')
|
||||
URIBuilder(scheme=None, userinfo=None, host='google.com',
|
||||
port=None, path=None, query=None, fragment=None)
|
||||
|
||||
"""
|
||||
return URIBuilder(
|
||||
scheme=self.scheme,
|
||||
userinfo=self.userinfo,
|
||||
host=normalizers.normalize_host(host),
|
||||
port=self.port,
|
||||
path=self.path,
|
||||
query=self.query,
|
||||
fragment=self.fragment,
|
||||
)
|
||||
|
||||
def add_port(self, port):
|
||||
"""Add port to the URI.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_port(80)
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port='80',
|
||||
path=None, query=None, fragment=None)
|
||||
|
||||
>>> URIBuilder().add_port(443)
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port='443',
|
||||
path=None, query=None, fragment=None)
|
||||
|
||||
"""
|
||||
port_int = int(port)
|
||||
if port_int < 0:
|
||||
raise ValueError(
|
||||
'ports are not allowed to be negative. You provided {}'.format(
|
||||
port_int,
|
||||
)
|
||||
)
|
||||
if port_int > 65535:
|
||||
raise ValueError(
|
||||
'ports are not allowed to be larger than 65535. '
|
||||
'You provided {}'.format(
|
||||
port_int,
|
||||
)
|
||||
)
|
||||
|
||||
return URIBuilder(
|
||||
scheme=self.scheme,
|
||||
userinfo=self.userinfo,
|
||||
host=self.host,
|
||||
port='{}'.format(port_int),
|
||||
path=self.path,
|
||||
query=self.query,
|
||||
fragment=self.fragment,
|
||||
)
|
||||
|
||||
def add_path(self, path):
|
||||
"""Add a path to the URI.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_path('sigmavirus24/rfc3985')
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
|
||||
path='/sigmavirus24/rfc3986', query=None, fragment=None)
|
||||
|
||||
>>> URIBuilder().add_path('/checkout.php')
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
|
||||
path='/checkout.php', query=None, fragment=None)
|
||||
|
||||
"""
|
||||
if not path.startswith('/'):
|
||||
path = '/{}'.format(path)
|
||||
|
||||
return URIBuilder(
|
||||
scheme=self.scheme,
|
||||
userinfo=self.userinfo,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
path=normalizers.normalize_path(path),
|
||||
query=self.query,
|
||||
fragment=self.fragment,
|
||||
)
|
||||
|
||||
def add_query_from(self, query_items):
|
||||
"""Generate and add a query a dictionary or list of tuples.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_query_from({'a': 'b c'})
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
|
||||
path=None, query='a=b+c', fragment=None)
|
||||
|
||||
>>> URIBuilder().add_query_from([('a', 'b c')])
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
|
||||
path=None, query='a=b+c', fragment=None)
|
||||
|
||||
"""
|
||||
query = normalizers.normalize_query(compat.urlencode(query_items))
|
||||
|
||||
return URIBuilder(
|
||||
scheme=self.scheme,
|
||||
userinfo=self.userinfo,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
path=self.path,
|
||||
query=query,
|
||||
fragment=self.fragment,
|
||||
)
|
||||
|
||||
def add_query(self, query):
|
||||
"""Add a pre-formated query string to the URI.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_query('a=b&c=d')
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
|
||||
path=None, query='a=b&c=d', fragment=None)
|
||||
|
||||
"""
|
||||
return URIBuilder(
|
||||
scheme=self.scheme,
|
||||
userinfo=self.userinfo,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
path=self.path,
|
||||
query=normalizers.normalize_query(query),
|
||||
fragment=self.fragment,
|
||||
)
|
||||
|
||||
def add_fragment(self, fragment):
|
||||
"""Add a fragment to the URI.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_fragment('section-2.6.1')
|
||||
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
|
||||
path=None, query=None, fragment='section-2.6.1')
|
||||
|
||||
"""
|
||||
return URIBuilder(
|
||||
scheme=self.scheme,
|
||||
userinfo=self.userinfo,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
path=self.path,
|
||||
query=self.query,
|
||||
fragment=normalizers.normalize_fragment(fragment),
|
||||
)
|
||||
|
||||
def finalize(self):
|
||||
"""Create a URIReference from our builder.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> URIBuilder().add_scheme('https').add_host('github.com'
|
||||
... ).add_path('sigmavirus24/rfc3986').finalize().unsplit()
|
||||
'https://github.com/sigmavirus24/rfc3986'
|
||||
|
||||
>>> URIBuilder().add_scheme('https').add_host('github.com'
|
||||
... ).add_path('sigmavirus24/rfc3986').add_credentials(
|
||||
... 'sigmavirus24', 'not-re@l').finalize().unsplit()
|
||||
'https://sigmavirus24:not-re%40l@github.com/sigmavirus24/rfc3986'
|
||||
|
||||
"""
|
||||
return uri.URIReference(
|
||||
self.scheme,
|
||||
normalizers.normalize_authority(
|
||||
(self.userinfo, self.host, self.port)
|
||||
),
|
||||
self.path,
|
||||
self.query,
|
||||
self.fragment,
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Rackspace
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Compatibility module for Python 2 and 3 support."""
|
||||
import sys
|
||||
|
||||
try:
|
||||
from urllib.parse import quote as urlquote
|
||||
except ImportError: # Python 2.x
|
||||
from urllib import quote as urlquote
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
except ImportError: # Python 2.x
|
||||
from urllib import urlencode
|
||||
|
||||
__all__ = (
|
||||
'to_bytes',
|
||||
'to_str',
|
||||
'urlquote',
|
||||
'urlencode',
|
||||
)
|
||||
|
||||
PY3 = (3, 0) <= sys.version_info < (4, 0)
|
||||
PY2 = (2, 6) <= sys.version_info < (2, 8)
|
||||
|
||||
|
||||
if PY3:
|
||||
unicode = str # Python 3.x
|
||||
|
||||
|
||||
def to_str(b, encoding='utf-8'):
|
||||
"""Ensure that b is text in the specified encoding."""
|
||||
if hasattr(b, 'decode') and not isinstance(b, unicode):
|
||||
b = b.decode(encoding)
|
||||
return b
|
||||
|
||||
|
||||
def to_bytes(s, encoding='utf-8'):
|
||||
"""Ensure that s is converted to bytes from the encoding."""
|
||||
if hasattr(s, 'encode') and not isinstance(s, bytes):
|
||||
s = s.encode(encoding)
|
||||
return s
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Exceptions module for rfc3986."""
|
||||
|
||||
from . import compat
|
||||
|
||||
|
||||
class RFC3986Exception(Exception):
|
||||
"""Base class for all rfc3986 exception classes."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAuthority(RFC3986Exception):
|
||||
"""Exception when the authority string is invalid."""
|
||||
|
||||
def __init__(self, authority):
|
||||
"""Initialize the exception with the invalid authority."""
|
||||
super(InvalidAuthority, self).__init__(
|
||||
u"The authority ({0}) is not valid.".format(
|
||||
compat.to_str(authority)))
|
||||
|
||||
|
||||
class InvalidPort(RFC3986Exception):
|
||||
"""Exception when the port is invalid."""
|
||||
|
||||
def __init__(self, port):
|
||||
"""Initialize the exception with the invalid port."""
|
||||
super(InvalidPort, self).__init__(
|
||||
'The port ("{0}") is not valid.'.format(port))
|
||||
|
||||
|
||||
class ResolutionError(RFC3986Exception):
|
||||
"""Exception to indicate a failure to resolve a URI."""
|
||||
|
||||
def __init__(self, uri):
|
||||
"""Initialize the error with the failed URI."""
|
||||
super(ResolutionError, self).__init__(
|
||||
"{0} is not an absolute URI.".format(uri.unsplit()))
|
||||
|
||||
|
||||
class ValidationError(RFC3986Exception):
|
||||
"""Exception raised during Validation of a URI."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingComponentError(ValidationError):
|
||||
"""Exception raised when a required component is missing."""
|
||||
|
||||
def __init__(self, uri, *component_names):
|
||||
"""Initialize the error with the missing component name."""
|
||||
verb = 'was'
|
||||
if len(component_names) > 1:
|
||||
verb = 'were'
|
||||
|
||||
self.uri = uri
|
||||
self.components = sorted(component_names)
|
||||
components = ', '.join(self.components)
|
||||
super(MissingComponentError, self).__init__(
|
||||
"{} {} required but missing".format(components, verb),
|
||||
uri,
|
||||
self.components,
|
||||
)
|
||||
|
||||
|
||||
class UnpermittedComponentError(ValidationError):
|
||||
"""Exception raised when a component has an unpermitted value."""
|
||||
|
||||
def __init__(self, component_name, component_value, allowed_values):
|
||||
"""Initialize the error with the unpermitted component."""
|
||||
super(UnpermittedComponentError, self).__init__(
|
||||
"{} was required to be one of {!r} but was {!r}".format(
|
||||
component_name, list(sorted(allowed_values)), component_value,
|
||||
),
|
||||
component_name,
|
||||
component_value,
|
||||
allowed_values,
|
||||
)
|
||||
self.component_name = component_name
|
||||
self.component_value = component_value
|
||||
self.allowed_values = allowed_values
|
||||
|
||||
|
||||
class PasswordForbidden(ValidationError):
|
||||
"""Exception raised when a URL has a password in the userinfo section."""
|
||||
|
||||
def __init__(self, uri):
|
||||
"""Initialize the error with the URI that failed validation."""
|
||||
unsplit = getattr(uri, 'unsplit', lambda: uri)
|
||||
super(PasswordForbidden, self).__init__(
|
||||
'"{}" contained a password when validation forbade it'.format(
|
||||
unsplit()
|
||||
)
|
||||
)
|
||||
self.uri = uri
|
||||
|
||||
|
||||
class InvalidComponentsError(ValidationError):
|
||||
"""Exception raised when one or more components are invalid."""
|
||||
|
||||
def __init__(self, uri, *component_names):
|
||||
"""Initialize the error with the invalid component name(s)."""
|
||||
verb = 'was'
|
||||
if len(component_names) > 1:
|
||||
verb = 'were'
|
||||
|
||||
self.uri = uri
|
||||
self.components = sorted(component_names)
|
||||
components = ', '.join(self.components)
|
||||
super(InvalidComponentsError, self).__init__(
|
||||
"{} {} found to be invalid".format(components, verb),
|
||||
uri,
|
||||
self.components,
|
||||
)
|
||||
|
||||
|
||||
class MissingDependencyError(RFC3986Exception):
|
||||
"""Exception raised when an IRI is encoded without the 'idna' module."""
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
"""Module containing the implementation of the IRIReference class."""
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Rackspace
|
||||
# Copyright (c) 2015 Ian Stapleton Cordasco
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from collections import namedtuple
|
||||
|
||||
from . import compat
|
||||
from . import exceptions
|
||||
from . import misc
|
||||
from . import normalizers
|
||||
from . import uri
|
||||
|
||||
|
||||
try:
|
||||
import idna
|
||||
except ImportError: # pragma: no cover
|
||||
idna = None
|
||||
|
||||
|
||||
class IRIReference(namedtuple('IRIReference', misc.URI_COMPONENTS),
|
||||
uri.URIMixin):
|
||||
"""Immutable object representing a parsed IRI Reference.
|
||||
|
||||
Can be encoded into an URIReference object via the procedure
|
||||
specified in RFC 3987 Section 3.1
|
||||
|
||||
.. note::
|
||||
The IRI submodule is a new interface and may possibly change in
|
||||
the future. Check for changes to the interface when upgrading.
|
||||
"""
|
||||
|
||||
slots = ()
|
||||
|
||||
def __new__(cls, scheme, authority, path, query, fragment,
|
||||
encoding='utf-8'):
|
||||
"""Create a new IRIReference."""
|
||||
ref = super(IRIReference, cls).__new__(
|
||||
cls,
|
||||
scheme or None,
|
||||
authority or None,
|
||||
path or None,
|
||||
query,
|
||||
fragment)
|
||||
ref.encoding = encoding
|
||||
return ref
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compare this reference to another."""
|
||||
other_ref = other
|
||||
if isinstance(other, tuple):
|
||||
other_ref = self.__class__(*other)
|
||||
elif not isinstance(other, IRIReference):
|
||||
try:
|
||||
other_ref = self.__class__.from_string(other)
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
'Unable to compare {0}() to {1}()'.format(
|
||||
type(self).__name__, type(other).__name__))
|
||||
|
||||
# See http://tools.ietf.org/html/rfc3986#section-6.2
|
||||
return tuple(self) == tuple(other_ref)
|
||||
|
||||
def _match_subauthority(self):
|
||||
return misc.ISUBAUTHORITY_MATCHER.match(self.authority)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, iri_string, encoding='utf-8'):
|
||||
"""Parse a IRI reference from the given unicode IRI string.
|
||||
|
||||
:param str iri_string: Unicode IRI to be parsed into a reference.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:returns: :class:`IRIReference` or subclass thereof
|
||||
"""
|
||||
iri_string = compat.to_str(iri_string, encoding)
|
||||
|
||||
split_iri = misc.IRI_MATCHER.match(iri_string).groupdict()
|
||||
return cls(
|
||||
split_iri['scheme'], split_iri['authority'],
|
||||
normalizers.encode_component(split_iri['path'], encoding),
|
||||
normalizers.encode_component(split_iri['query'], encoding),
|
||||
normalizers.encode_component(split_iri['fragment'], encoding),
|
||||
encoding,
|
||||
)
|
||||
|
||||
def encode(self, idna_encoder=None): # noqa: C901
|
||||
"""Encode an IRIReference into a URIReference instance.
|
||||
|
||||
If the ``idna`` module is installed or the ``rfc3986[idna]``
|
||||
extra is used then unicode characters in the IRI host
|
||||
component will be encoded with IDNA2008.
|
||||
|
||||
:param idna_encoder:
|
||||
Function that encodes each part of the host component
|
||||
If not given will raise an exception if the IRI
|
||||
contains a host component.
|
||||
:rtype: uri.URIReference
|
||||
:returns: A URI reference
|
||||
"""
|
||||
authority = self.authority
|
||||
if authority:
|
||||
if idna_encoder is None:
|
||||
if idna is None: # pragma: no cover
|
||||
raise exceptions.MissingDependencyError(
|
||||
"Could not import the 'idna' module "
|
||||
"and the IRI hostname requires encoding"
|
||||
)
|
||||
|
||||
def idna_encoder(name):
|
||||
if any(ord(c) > 128 for c in name):
|
||||
try:
|
||||
return idna.encode(name.lower(),
|
||||
strict=True,
|
||||
std3_rules=True)
|
||||
except idna.IDNAError:
|
||||
raise exceptions.InvalidAuthority(self.authority)
|
||||
return name
|
||||
|
||||
authority = ""
|
||||
if self.host:
|
||||
authority = ".".join([compat.to_str(idna_encoder(part))
|
||||
for part in self.host.split(".")])
|
||||
|
||||
if self.userinfo is not None:
|
||||
authority = (normalizers.encode_component(
|
||||
self.userinfo, self.encoding) + '@' + authority)
|
||||
|
||||
if self.port is not None:
|
||||
authority += ":" + str(self.port)
|
||||
|
||||
return uri.URIReference(self.scheme,
|
||||
authority,
|
||||
path=self.path,
|
||||
query=self.query,
|
||||
fragment=self.fragment,
|
||||
encoding=self.encoding)
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Rackspace
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Module containing compiled regular expressions and constants.
|
||||
|
||||
This module contains important constants, patterns, and compiled regular
|
||||
expressions for parsing and validating URIs and their components.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from . import abnf_regexp
|
||||
|
||||
# These are enumerated for the named tuple used as a superclass of
|
||||
# URIReference
|
||||
URI_COMPONENTS = ['scheme', 'authority', 'path', 'query', 'fragment']
|
||||
|
||||
important_characters = {
|
||||
'generic_delimiters': abnf_regexp.GENERIC_DELIMITERS,
|
||||
'sub_delimiters': abnf_regexp.SUB_DELIMITERS,
|
||||
# We need to escape the '*' in this case
|
||||
're_sub_delimiters': abnf_regexp.SUB_DELIMITERS_RE,
|
||||
'unreserved_chars': abnf_regexp.UNRESERVED_CHARS,
|
||||
# We need to escape the '-' in this case:
|
||||
're_unreserved': abnf_regexp.UNRESERVED_RE,
|
||||
}
|
||||
|
||||
# For details about delimiters and reserved characters, see:
|
||||
# http://tools.ietf.org/html/rfc3986#section-2.2
|
||||
GENERIC_DELIMITERS = abnf_regexp.GENERIC_DELIMITERS_SET
|
||||
SUB_DELIMITERS = abnf_regexp.SUB_DELIMITERS_SET
|
||||
RESERVED_CHARS = abnf_regexp.RESERVED_CHARS_SET
|
||||
# For details about unreserved characters, see:
|
||||
# http://tools.ietf.org/html/rfc3986#section-2.3
|
||||
UNRESERVED_CHARS = abnf_regexp.UNRESERVED_CHARS_SET
|
||||
NON_PCT_ENCODED = abnf_regexp.NON_PCT_ENCODED_SET
|
||||
|
||||
URI_MATCHER = re.compile(abnf_regexp.URL_PARSING_RE)
|
||||
|
||||
SUBAUTHORITY_MATCHER = re.compile((
|
||||
'^(?:(?P<userinfo>{0})@)?' # userinfo
|
||||
'(?P<host>{1})' # host
|
||||
':?(?P<port>{2})?$' # port
|
||||
).format(abnf_regexp.USERINFO_RE,
|
||||
abnf_regexp.HOST_PATTERN,
|
||||
abnf_regexp.PORT_RE))
|
||||
|
||||
|
||||
HOST_MATCHER = re.compile('^' + abnf_regexp.HOST_RE + '$')
|
||||
IPv4_MATCHER = re.compile('^' + abnf_regexp.IPv4_RE + '$')
|
||||
IPv6_MATCHER = re.compile(r'^\[' + abnf_regexp.IPv6_ADDRZ_RFC4007_RE + r'\]$')
|
||||
|
||||
# Used by host validator
|
||||
IPv6_NO_RFC4007_MATCHER = re.compile(r'^\[%s\]$' % (
|
||||
abnf_regexp.IPv6_ADDRZ_RE
|
||||
))
|
||||
|
||||
# Matcher used to validate path components
|
||||
PATH_MATCHER = re.compile(abnf_regexp.PATH_RE)
|
||||
|
||||
|
||||
# ##################################
|
||||
# Query and Fragment Matcher Section
|
||||
# ##################################
|
||||
|
||||
QUERY_MATCHER = re.compile(abnf_regexp.QUERY_RE)
|
||||
|
||||
FRAGMENT_MATCHER = QUERY_MATCHER
|
||||
|
||||
# Scheme validation, see: http://tools.ietf.org/html/rfc3986#section-3.1
|
||||
SCHEME_MATCHER = re.compile('^{0}$'.format(abnf_regexp.SCHEME_RE))
|
||||
|
||||
RELATIVE_REF_MATCHER = re.compile(r'^%s(\?%s)?(#%s)?$' % (
|
||||
abnf_regexp.RELATIVE_PART_RE,
|
||||
abnf_regexp.QUERY_RE,
|
||||
abnf_regexp.FRAGMENT_RE,
|
||||
))
|
||||
|
||||
# See http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
ABSOLUTE_URI_MATCHER = re.compile(r'^%s:%s(\?%s)?$' % (
|
||||
abnf_regexp.COMPONENT_PATTERN_DICT['scheme'],
|
||||
abnf_regexp.HIER_PART_RE,
|
||||
abnf_regexp.QUERY_RE[1:-1],
|
||||
))
|
||||
|
||||
# ###############
|
||||
# IRIs / RFC 3987
|
||||
# ###############
|
||||
|
||||
IRI_MATCHER = re.compile(abnf_regexp.URL_PARSING_RE, re.UNICODE)
|
||||
|
||||
ISUBAUTHORITY_MATCHER = re.compile((
|
||||
u'^(?:(?P<userinfo>{0})@)?' # iuserinfo
|
||||
u'(?P<host>{1})' # ihost
|
||||
u':?(?P<port>{2})?$' # port
|
||||
).format(abnf_regexp.IUSERINFO_RE,
|
||||
abnf_regexp.IHOST_RE,
|
||||
abnf_regexp.PORT_RE), re.UNICODE)
|
||||
|
||||
|
||||
IHOST_MATCHER = re.compile('^' + abnf_regexp.IHOST_RE + '$', re.UNICODE)
|
||||
|
||||
IPATH_MATCHER = re.compile(abnf_regexp.IPATH_RE, re.UNICODE)
|
||||
|
||||
IQUERY_MATCHER = re.compile(abnf_regexp.IQUERY_RE, re.UNICODE)
|
||||
|
||||
IFRAGMENT_MATCHER = re.compile(abnf_regexp.IFRAGMENT_RE, re.UNICODE)
|
||||
|
||||
|
||||
RELATIVE_IRI_MATCHER = re.compile(u'^%s(?:\\?%s)?(?:%s)?$' % (
|
||||
abnf_regexp.IRELATIVE_PART_RE,
|
||||
abnf_regexp.IQUERY_RE,
|
||||
abnf_regexp.IFRAGMENT_RE
|
||||
), re.UNICODE)
|
||||
|
||||
ABSOLUTE_IRI_MATCHER = re.compile(u'^%s:%s(?:\\?%s)?$' % (
|
||||
abnf_regexp.COMPONENT_PATTERN_DICT['scheme'],
|
||||
abnf_regexp.IHIER_PART_RE,
|
||||
abnf_regexp.IQUERY_RE[1:-1]
|
||||
), re.UNICODE)
|
||||
|
||||
|
||||
# Path merger as defined in http://tools.ietf.org/html/rfc3986#section-5.2.3
|
||||
def merge_paths(base_uri, relative_path):
|
||||
"""Merge a base URI's path with a relative URI's path."""
|
||||
if base_uri.path is None and base_uri.authority is not None:
|
||||
return '/' + relative_path
|
||||
else:
|
||||
path = base_uri.path or ''
|
||||
index = path.rfind('/')
|
||||
return path[:index] + '/' + relative_path
|
||||
|
||||
|
||||
UseExisting = object()
|
||||
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Rackspace
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Module with functions to normalize components."""
|
||||
import re
|
||||
|
||||
from . import compat
|
||||
from . import misc
|
||||
|
||||
|
||||
def normalize_scheme(scheme):
|
||||
"""Normalize the scheme component."""
|
||||
return scheme.lower()
|
||||
|
||||
|
||||
def normalize_authority(authority):
|
||||
"""Normalize an authority tuple to a string."""
|
||||
userinfo, host, port = authority
|
||||
result = ''
|
||||
if userinfo:
|
||||
result += normalize_percent_characters(userinfo) + '@'
|
||||
if host:
|
||||
result += normalize_host(host)
|
||||
if port:
|
||||
result += ':' + port
|
||||
return result
|
||||
|
||||
|
||||
def normalize_username(username):
|
||||
"""Normalize a username to make it safe to include in userinfo."""
|
||||
return compat.urlquote(username)
|
||||
|
||||
|
||||
def normalize_password(password):
|
||||
"""Normalize a password to make safe for userinfo."""
|
||||
return compat.urlquote(password)
|
||||
|
||||
|
||||
def normalize_host(host):
|
||||
"""Normalize a host string."""
|
||||
if misc.IPv6_MATCHER.match(host):
|
||||
percent = host.find('%')
|
||||
if percent != -1:
|
||||
percent_25 = host.find('%25')
|
||||
|
||||
# Replace RFC 4007 IPv6 Zone ID delimiter '%' with '%25'
|
||||
# from RFC 6874. If the host is '[<IPv6 addr>%25]' then we
|
||||
# assume RFC 4007 and normalize to '[<IPV6 addr>%2525]'
|
||||
if percent_25 == -1 or percent < percent_25 or \
|
||||
(percent == percent_25 and percent_25 == len(host) - 4):
|
||||
host = host.replace('%', '%25', 1)
|
||||
|
||||
# Don't normalize the casing of the Zone ID
|
||||
return host[:percent].lower() + host[percent:]
|
||||
|
||||
return host.lower()
|
||||
|
||||
|
||||
def normalize_path(path):
|
||||
"""Normalize the path string."""
|
||||
if not path:
|
||||
return path
|
||||
|
||||
path = normalize_percent_characters(path)
|
||||
return remove_dot_segments(path)
|
||||
|
||||
|
||||
def normalize_query(query):
|
||||
"""Normalize the query string."""
|
||||
if not query:
|
||||
return query
|
||||
return normalize_percent_characters(query)
|
||||
|
||||
|
||||
def normalize_fragment(fragment):
|
||||
"""Normalize the fragment string."""
|
||||
if not fragment:
|
||||
return fragment
|
||||
return normalize_percent_characters(fragment)
|
||||
|
||||
|
||||
PERCENT_MATCHER = re.compile('%[A-Fa-f0-9]{2}')
|
||||
|
||||
|
||||
def normalize_percent_characters(s):
|
||||
"""All percent characters should be upper-cased.
|
||||
|
||||
For example, ``"%3afoo%DF%ab"`` should be turned into ``"%3Afoo%DF%AB"``.
|
||||
"""
|
||||
matches = set(PERCENT_MATCHER.findall(s))
|
||||
for m in matches:
|
||||
if not m.isupper():
|
||||
s = s.replace(m, m.upper())
|
||||
return s
|
||||
|
||||
|
||||
def remove_dot_segments(s):
|
||||
"""Remove dot segments from the string.
|
||||
|
||||
See also Section 5.2.4 of :rfc:`3986`.
|
||||
"""
|
||||
# See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code
|
||||
segments = s.split('/') # Turn the path into a list of segments
|
||||
output = [] # Initialize the variable to use to store output
|
||||
|
||||
for segment in segments:
|
||||
# '.' is the current directory, so ignore it, it is superfluous
|
||||
if segment == '.':
|
||||
continue
|
||||
# Anything other than '..', should be appended to the output
|
||||
elif segment != '..':
|
||||
output.append(segment)
|
||||
# In this case segment == '..', if we can, we should pop the last
|
||||
# element
|
||||
elif output:
|
||||
output.pop()
|
||||
|
||||
# If the path starts with '/' and the output is empty or the first string
|
||||
# is non-empty
|
||||
if s.startswith('/') and (not output or output[0]):
|
||||
output.insert(0, '')
|
||||
|
||||
# If the path starts with '/.' or '/..' ensure we add one more empty
|
||||
# string to add a trailing '/'
|
||||
if s.endswith(('/.', '/..')):
|
||||
output.append('')
|
||||
|
||||
return '/'.join(output)
|
||||
|
||||
|
||||
def encode_component(uri_component, encoding):
|
||||
"""Encode the specific component in the provided encoding."""
|
||||
if uri_component is None:
|
||||
return uri_component
|
||||
|
||||
# Try to see if the component we're encoding is already percent-encoded
|
||||
# so we can skip all '%' characters but still encode all others.
|
||||
percent_encodings = len(PERCENT_MATCHER.findall(
|
||||
compat.to_str(uri_component, encoding)))
|
||||
|
||||
uri_bytes = compat.to_bytes(uri_component, encoding)
|
||||
is_percent_encoded = percent_encodings == uri_bytes.count(b'%')
|
||||
|
||||
encoded_uri = bytearray()
|
||||
|
||||
for i in range(0, len(uri_bytes)):
|
||||
# Will return a single character bytestring on both Python 2 & 3
|
||||
byte = uri_bytes[i:i+1]
|
||||
byte_ord = ord(byte)
|
||||
if ((is_percent_encoded and byte == b'%')
|
||||
or (byte_ord < 128 and byte.decode() in misc.NON_PCT_ENCODED)):
|
||||
encoded_uri.extend(byte)
|
||||
continue
|
||||
encoded_uri.extend('%{0:02x}'.format(byte_ord).encode().upper())
|
||||
|
||||
return encoded_uri.decode(encoding)
|
||||
@@ -0,0 +1,385 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015 Ian Stapleton Cordasco
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Module containing the urlparse compatibility logic."""
|
||||
from collections import namedtuple
|
||||
|
||||
from . import compat
|
||||
from . import exceptions
|
||||
from . import misc
|
||||
from . import normalizers
|
||||
from . import uri
|
||||
|
||||
__all__ = ('ParseResult', 'ParseResultBytes')
|
||||
|
||||
PARSED_COMPONENTS = ('scheme', 'userinfo', 'host', 'port', 'path', 'query',
|
||||
'fragment')
|
||||
|
||||
|
||||
class ParseResultMixin(object):
|
||||
def _generate_authority(self, attributes):
|
||||
# I swear I did not align the comparisons below. That's just how they
|
||||
# happened to align based on pep8 and attribute lengths.
|
||||
userinfo, host, port = (attributes[p]
|
||||
for p in ('userinfo', 'host', 'port'))
|
||||
if (self.userinfo != userinfo or
|
||||
self.host != host or
|
||||
self.port != port):
|
||||
if port:
|
||||
port = '{0}'.format(port)
|
||||
return normalizers.normalize_authority(
|
||||
(compat.to_str(userinfo, self.encoding),
|
||||
compat.to_str(host, self.encoding),
|
||||
port)
|
||||
)
|
||||
return self.authority
|
||||
|
||||
def geturl(self):
|
||||
"""Shim to match the standard library method."""
|
||||
return self.unsplit()
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
"""Shim to match the standard library."""
|
||||
return self.host
|
||||
|
||||
@property
|
||||
def netloc(self):
|
||||
"""Shim to match the standard library."""
|
||||
return self.authority
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
"""Shim to match the standard library."""
|
||||
return self.query
|
||||
|
||||
|
||||
class ParseResult(namedtuple('ParseResult', PARSED_COMPONENTS),
|
||||
ParseResultMixin):
|
||||
"""Implementation of urlparse compatibility class.
|
||||
|
||||
This uses the URIReference logic to handle compatibility with the
|
||||
urlparse.ParseResult class.
|
||||
"""
|
||||
|
||||
slots = ()
|
||||
|
||||
def __new__(cls, scheme, userinfo, host, port, path, query, fragment,
|
||||
uri_ref, encoding='utf-8'):
|
||||
"""Create a new ParseResult."""
|
||||
parse_result = super(ParseResult, cls).__new__(
|
||||
cls,
|
||||
scheme or None,
|
||||
userinfo or None,
|
||||
host,
|
||||
port or None,
|
||||
path or None,
|
||||
query,
|
||||
fragment)
|
||||
parse_result.encoding = encoding
|
||||
parse_result.reference = uri_ref
|
||||
return parse_result
|
||||
|
||||
@classmethod
|
||||
def from_parts(cls, scheme=None, userinfo=None, host=None, port=None,
|
||||
path=None, query=None, fragment=None, encoding='utf-8'):
|
||||
"""Create a ParseResult instance from its parts."""
|
||||
authority = ''
|
||||
if userinfo is not None:
|
||||
authority += userinfo + '@'
|
||||
if host is not None:
|
||||
authority += host
|
||||
if port is not None:
|
||||
authority += ':{0}'.format(port)
|
||||
uri_ref = uri.URIReference(scheme=scheme,
|
||||
authority=authority,
|
||||
path=path,
|
||||
query=query,
|
||||
fragment=fragment,
|
||||
encoding=encoding).normalize()
|
||||
userinfo, host, port = authority_from(uri_ref, strict=True)
|
||||
return cls(scheme=uri_ref.scheme,
|
||||
userinfo=userinfo,
|
||||
host=host,
|
||||
port=port,
|
||||
path=uri_ref.path,
|
||||
query=uri_ref.query,
|
||||
fragment=uri_ref.fragment,
|
||||
uri_ref=uri_ref,
|
||||
encoding=encoding)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, uri_string, encoding='utf-8', strict=True,
|
||||
lazy_normalize=True):
|
||||
"""Parse a URI from the given unicode URI string.
|
||||
|
||||
:param str uri_string: Unicode URI to be parsed into a reference.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:param bool strict: Parse strictly according to :rfc:`3986` if True.
|
||||
If False, parse similarly to the standard library's urlparse
|
||||
function.
|
||||
:returns: :class:`ParseResult` or subclass thereof
|
||||
"""
|
||||
reference = uri.URIReference.from_string(uri_string, encoding)
|
||||
if not lazy_normalize:
|
||||
reference = reference.normalize()
|
||||
userinfo, host, port = authority_from(reference, strict)
|
||||
|
||||
return cls(scheme=reference.scheme,
|
||||
userinfo=userinfo,
|
||||
host=host,
|
||||
port=port,
|
||||
path=reference.path,
|
||||
query=reference.query,
|
||||
fragment=reference.fragment,
|
||||
uri_ref=reference,
|
||||
encoding=encoding)
|
||||
|
||||
@property
|
||||
def authority(self):
|
||||
"""Return the normalized authority."""
|
||||
return self.reference.authority
|
||||
|
||||
def copy_with(self, scheme=misc.UseExisting, userinfo=misc.UseExisting,
|
||||
host=misc.UseExisting, port=misc.UseExisting,
|
||||
path=misc.UseExisting, query=misc.UseExisting,
|
||||
fragment=misc.UseExisting):
|
||||
"""Create a copy of this instance replacing with specified parts."""
|
||||
attributes = zip(PARSED_COMPONENTS,
|
||||
(scheme, userinfo, host, port, path, query, fragment))
|
||||
attrs_dict = {}
|
||||
for name, value in attributes:
|
||||
if value is misc.UseExisting:
|
||||
value = getattr(self, name)
|
||||
attrs_dict[name] = value
|
||||
authority = self._generate_authority(attrs_dict)
|
||||
ref = self.reference.copy_with(scheme=attrs_dict['scheme'],
|
||||
authority=authority,
|
||||
path=attrs_dict['path'],
|
||||
query=attrs_dict['query'],
|
||||
fragment=attrs_dict['fragment'])
|
||||
return ParseResult(uri_ref=ref, encoding=self.encoding, **attrs_dict)
|
||||
|
||||
def encode(self, encoding=None):
|
||||
"""Convert to an instance of ParseResultBytes."""
|
||||
encoding = encoding or self.encoding
|
||||
attrs = dict(
|
||||
zip(PARSED_COMPONENTS,
|
||||
(attr.encode(encoding) if hasattr(attr, 'encode') else attr
|
||||
for attr in self)))
|
||||
return ParseResultBytes(
|
||||
uri_ref=self.reference,
|
||||
encoding=encoding,
|
||||
**attrs
|
||||
)
|
||||
|
||||
def unsplit(self, use_idna=False):
|
||||
"""Create a URI string from the components.
|
||||
|
||||
:returns: The parsed URI reconstituted as a string.
|
||||
:rtype: str
|
||||
"""
|
||||
parse_result = self
|
||||
if use_idna and self.host:
|
||||
hostbytes = self.host.encode('idna')
|
||||
host = hostbytes.decode(self.encoding)
|
||||
parse_result = self.copy_with(host=host)
|
||||
return parse_result.reference.unsplit()
|
||||
|
||||
|
||||
class ParseResultBytes(namedtuple('ParseResultBytes', PARSED_COMPONENTS),
|
||||
ParseResultMixin):
|
||||
"""Compatibility shim for the urlparse.ParseResultBytes object."""
|
||||
|
||||
def __new__(cls, scheme, userinfo, host, port, path, query, fragment,
|
||||
uri_ref, encoding='utf-8', lazy_normalize=True):
|
||||
"""Create a new ParseResultBytes instance."""
|
||||
parse_result = super(ParseResultBytes, cls).__new__(
|
||||
cls,
|
||||
scheme or None,
|
||||
userinfo or None,
|
||||
host,
|
||||
port or None,
|
||||
path or None,
|
||||
query or None,
|
||||
fragment or None)
|
||||
parse_result.encoding = encoding
|
||||
parse_result.reference = uri_ref
|
||||
parse_result.lazy_normalize = lazy_normalize
|
||||
return parse_result
|
||||
|
||||
@classmethod
|
||||
def from_parts(cls, scheme=None, userinfo=None, host=None, port=None,
|
||||
path=None, query=None, fragment=None, encoding='utf-8',
|
||||
lazy_normalize=True):
|
||||
"""Create a ParseResult instance from its parts."""
|
||||
authority = ''
|
||||
if userinfo is not None:
|
||||
authority += userinfo + '@'
|
||||
if host is not None:
|
||||
authority += host
|
||||
if port is not None:
|
||||
authority += ':{0}'.format(int(port))
|
||||
uri_ref = uri.URIReference(scheme=scheme,
|
||||
authority=authority,
|
||||
path=path,
|
||||
query=query,
|
||||
fragment=fragment,
|
||||
encoding=encoding)
|
||||
if not lazy_normalize:
|
||||
uri_ref = uri_ref.normalize()
|
||||
to_bytes = compat.to_bytes
|
||||
userinfo, host, port = authority_from(uri_ref, strict=True)
|
||||
return cls(scheme=to_bytes(scheme, encoding),
|
||||
userinfo=to_bytes(userinfo, encoding),
|
||||
host=to_bytes(host, encoding),
|
||||
port=port,
|
||||
path=to_bytes(path, encoding),
|
||||
query=to_bytes(query, encoding),
|
||||
fragment=to_bytes(fragment, encoding),
|
||||
uri_ref=uri_ref,
|
||||
encoding=encoding,
|
||||
lazy_normalize=lazy_normalize)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, uri_string, encoding='utf-8', strict=True,
|
||||
lazy_normalize=True):
|
||||
"""Parse a URI from the given unicode URI string.
|
||||
|
||||
:param str uri_string: Unicode URI to be parsed into a reference.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:param bool strict: Parse strictly according to :rfc:`3986` if True.
|
||||
If False, parse similarly to the standard library's urlparse
|
||||
function.
|
||||
:returns: :class:`ParseResultBytes` or subclass thereof
|
||||
"""
|
||||
reference = uri.URIReference.from_string(uri_string, encoding)
|
||||
if not lazy_normalize:
|
||||
reference = reference.normalize()
|
||||
userinfo, host, port = authority_from(reference, strict)
|
||||
|
||||
to_bytes = compat.to_bytes
|
||||
return cls(scheme=to_bytes(reference.scheme, encoding),
|
||||
userinfo=to_bytes(userinfo, encoding),
|
||||
host=to_bytes(host, encoding),
|
||||
port=port,
|
||||
path=to_bytes(reference.path, encoding),
|
||||
query=to_bytes(reference.query, encoding),
|
||||
fragment=to_bytes(reference.fragment, encoding),
|
||||
uri_ref=reference,
|
||||
encoding=encoding,
|
||||
lazy_normalize=lazy_normalize)
|
||||
|
||||
@property
|
||||
def authority(self):
|
||||
"""Return the normalized authority."""
|
||||
return self.reference.authority.encode(self.encoding)
|
||||
|
||||
def copy_with(self, scheme=misc.UseExisting, userinfo=misc.UseExisting,
|
||||
host=misc.UseExisting, port=misc.UseExisting,
|
||||
path=misc.UseExisting, query=misc.UseExisting,
|
||||
fragment=misc.UseExisting, lazy_normalize=True):
|
||||
"""Create a copy of this instance replacing with specified parts."""
|
||||
attributes = zip(PARSED_COMPONENTS,
|
||||
(scheme, userinfo, host, port, path, query, fragment))
|
||||
attrs_dict = {}
|
||||
for name, value in attributes:
|
||||
if value is misc.UseExisting:
|
||||
value = getattr(self, name)
|
||||
if not isinstance(value, bytes) and hasattr(value, 'encode'):
|
||||
value = value.encode(self.encoding)
|
||||
attrs_dict[name] = value
|
||||
authority = self._generate_authority(attrs_dict)
|
||||
to_str = compat.to_str
|
||||
ref = self.reference.copy_with(
|
||||
scheme=to_str(attrs_dict['scheme'], self.encoding),
|
||||
authority=to_str(authority, self.encoding),
|
||||
path=to_str(attrs_dict['path'], self.encoding),
|
||||
query=to_str(attrs_dict['query'], self.encoding),
|
||||
fragment=to_str(attrs_dict['fragment'], self.encoding)
|
||||
)
|
||||
if not lazy_normalize:
|
||||
ref = ref.normalize()
|
||||
return ParseResultBytes(
|
||||
uri_ref=ref,
|
||||
encoding=self.encoding,
|
||||
lazy_normalize=lazy_normalize,
|
||||
**attrs_dict
|
||||
)
|
||||
|
||||
def unsplit(self, use_idna=False):
|
||||
"""Create a URI bytes object from the components.
|
||||
|
||||
:returns: The parsed URI reconstituted as a string.
|
||||
:rtype: bytes
|
||||
"""
|
||||
parse_result = self
|
||||
if use_idna and self.host:
|
||||
# self.host is bytes, to encode to idna, we need to decode it
|
||||
# first
|
||||
host = self.host.decode(self.encoding)
|
||||
hostbytes = host.encode('idna')
|
||||
parse_result = self.copy_with(host=hostbytes)
|
||||
if self.lazy_normalize:
|
||||
parse_result = parse_result.copy_with(lazy_normalize=False)
|
||||
uri = parse_result.reference.unsplit()
|
||||
return uri.encode(self.encoding)
|
||||
|
||||
|
||||
def split_authority(authority):
|
||||
# Initialize our expected return values
|
||||
userinfo = host = port = None
|
||||
# Initialize an extra var we may need to use
|
||||
extra_host = None
|
||||
# Set-up rest in case there is no userinfo portion
|
||||
rest = authority
|
||||
|
||||
if '@' in authority:
|
||||
userinfo, rest = authority.rsplit('@', 1)
|
||||
|
||||
# Handle IPv6 host addresses
|
||||
if rest.startswith('['):
|
||||
host, rest = rest.split(']', 1)
|
||||
host += ']'
|
||||
|
||||
if ':' in rest:
|
||||
extra_host, port = rest.split(':', 1)
|
||||
elif not host and rest:
|
||||
host = rest
|
||||
|
||||
if extra_host and not host:
|
||||
host = extra_host
|
||||
|
||||
return userinfo, host, port
|
||||
|
||||
|
||||
def authority_from(reference, strict):
|
||||
try:
|
||||
subauthority = reference.authority_info()
|
||||
except exceptions.InvalidAuthority:
|
||||
if strict:
|
||||
raise
|
||||
userinfo, host, port = split_authority(reference.authority)
|
||||
else:
|
||||
# Thanks to Richard Barrell for this idea:
|
||||
# https://twitter.com/0x2ba22e11/status/617338811975139328
|
||||
userinfo, host, port = (subauthority.get(p)
|
||||
for p in ('userinfo', 'host', 'port'))
|
||||
|
||||
if port:
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
raise exceptions.InvalidPort(port)
|
||||
return userinfo, host, port
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
"""Module containing the implementation of the URIReference class."""
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014 Rackspace
|
||||
# Copyright (c) 2015 Ian Stapleton Cordasco
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from collections import namedtuple
|
||||
|
||||
from . import compat
|
||||
from . import misc
|
||||
from . import normalizers
|
||||
from ._mixin import URIMixin
|
||||
|
||||
|
||||
class URIReference(namedtuple('URIReference', misc.URI_COMPONENTS), URIMixin):
|
||||
"""Immutable object representing a parsed URI Reference.
|
||||
|
||||
.. note::
|
||||
|
||||
This class is not intended to be directly instantiated by the user.
|
||||
|
||||
This object exposes attributes for the following components of a
|
||||
URI:
|
||||
|
||||
- scheme
|
||||
- authority
|
||||
- path
|
||||
- query
|
||||
- fragment
|
||||
|
||||
.. attribute:: scheme
|
||||
|
||||
The scheme that was parsed for the URI Reference. For example,
|
||||
``http``, ``https``, ``smtp``, ``imap``, etc.
|
||||
|
||||
.. attribute:: authority
|
||||
|
||||
Component of the URI that contains the user information, host,
|
||||
and port sub-components. For example,
|
||||
``google.com``, ``127.0.0.1:5000``, ``username@[::1]``,
|
||||
``username:password@example.com:443``, etc.
|
||||
|
||||
.. attribute:: path
|
||||
|
||||
The path that was parsed for the given URI Reference. For example,
|
||||
``/``, ``/index.php``, etc.
|
||||
|
||||
.. attribute:: query
|
||||
|
||||
The query component for a given URI Reference. For example, ``a=b``,
|
||||
``a=b%20c``, ``a=b+c``, ``a=b,c=d,e=%20f``, etc.
|
||||
|
||||
.. attribute:: fragment
|
||||
|
||||
The fragment component of a URI. For example, ``section-3.1``.
|
||||
|
||||
This class also provides extra attributes for easier access to information
|
||||
like the subcomponents of the authority component.
|
||||
|
||||
.. attribute:: userinfo
|
||||
|
||||
The user information parsed from the authority.
|
||||
|
||||
.. attribute:: host
|
||||
|
||||
The hostname, IPv4, or IPv6 adddres parsed from the authority.
|
||||
|
||||
.. attribute:: port
|
||||
|
||||
The port parsed from the authority.
|
||||
"""
|
||||
|
||||
slots = ()
|
||||
|
||||
def __new__(cls, scheme, authority, path, query, fragment,
|
||||
encoding='utf-8'):
|
||||
"""Create a new URIReference."""
|
||||
ref = super(URIReference, cls).__new__(
|
||||
cls,
|
||||
scheme or None,
|
||||
authority or None,
|
||||
path or None,
|
||||
query,
|
||||
fragment)
|
||||
ref.encoding = encoding
|
||||
return ref
|
||||
|
||||
__hash__ = tuple.__hash__
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compare this reference to another."""
|
||||
other_ref = other
|
||||
if isinstance(other, tuple):
|
||||
other_ref = URIReference(*other)
|
||||
elif not isinstance(other, URIReference):
|
||||
try:
|
||||
other_ref = URIReference.from_string(other)
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
'Unable to compare URIReference() to {0}()'.format(
|
||||
type(other).__name__))
|
||||
|
||||
# See http://tools.ietf.org/html/rfc3986#section-6.2
|
||||
naive_equality = tuple(self) == tuple(other_ref)
|
||||
return naive_equality or self.normalized_equality(other_ref)
|
||||
|
||||
def normalize(self):
|
||||
"""Normalize this reference as described in Section 6.2.2.
|
||||
|
||||
This is not an in-place normalization. Instead this creates a new
|
||||
URIReference.
|
||||
|
||||
:returns: A new reference object with normalized components.
|
||||
:rtype: URIReference
|
||||
"""
|
||||
# See http://tools.ietf.org/html/rfc3986#section-6.2.2 for logic in
|
||||
# this method.
|
||||
return URIReference(normalizers.normalize_scheme(self.scheme or ''),
|
||||
normalizers.normalize_authority(
|
||||
(self.userinfo, self.host, self.port)),
|
||||
normalizers.normalize_path(self.path or ''),
|
||||
normalizers.normalize_query(self.query),
|
||||
normalizers.normalize_fragment(self.fragment),
|
||||
self.encoding)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, uri_string, encoding='utf-8'):
|
||||
"""Parse a URI reference from the given unicode URI string.
|
||||
|
||||
:param str uri_string: Unicode URI to be parsed into a reference.
|
||||
:param str encoding: The encoding of the string provided
|
||||
:returns: :class:`URIReference` or subclass thereof
|
||||
"""
|
||||
uri_string = compat.to_str(uri_string, encoding)
|
||||
|
||||
split_uri = misc.URI_MATCHER.match(uri_string).groupdict()
|
||||
return cls(
|
||||
split_uri['scheme'], split_uri['authority'],
|
||||
normalizers.encode_component(split_uri['path'], encoding),
|
||||
normalizers.encode_component(split_uri['query'], encoding),
|
||||
normalizers.encode_component(split_uri['fragment'], encoding),
|
||||
encoding,
|
||||
)
|
||||
@@ -0,0 +1,450 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Ian Stapleton Cordasco
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Module containing the validation logic for rfc3986."""
|
||||
from . import exceptions
|
||||
from . import misc
|
||||
from . import normalizers
|
||||
|
||||
|
||||
class Validator(object):
|
||||
"""Object used to configure validation of all objects in rfc3986.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
Example usage::
|
||||
|
||||
>>> from rfc3986 import api, validators
|
||||
>>> uri = api.uri_reference('https://github.com/')
|
||||
>>> validator = validators.Validator().require_presence_of(
|
||||
... 'scheme', 'host', 'path',
|
||||
... ).allow_schemes(
|
||||
... 'http', 'https',
|
||||
... ).allow_hosts(
|
||||
... '127.0.0.1', 'github.com',
|
||||
... )
|
||||
>>> validator.validate(uri)
|
||||
>>> invalid_uri = rfc3986.uri_reference('imap://mail.google.com')
|
||||
>>> validator.validate(invalid_uri)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
rfc3986.exceptions.MissingComponentError: ('path was required but
|
||||
missing', URIReference(scheme=u'imap', authority=u'mail.google.com',
|
||||
path=None, query=None, fragment=None), ['path'])
|
||||
|
||||
"""
|
||||
|
||||
COMPONENT_NAMES = frozenset([
|
||||
'scheme',
|
||||
'userinfo',
|
||||
'host',
|
||||
'port',
|
||||
'path',
|
||||
'query',
|
||||
'fragment',
|
||||
])
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize our default validations."""
|
||||
self.allowed_schemes = set()
|
||||
self.allowed_hosts = set()
|
||||
self.allowed_ports = set()
|
||||
self.allow_password = True
|
||||
self.required_components = {
|
||||
'scheme': False,
|
||||
'userinfo': False,
|
||||
'host': False,
|
||||
'port': False,
|
||||
'path': False,
|
||||
'query': False,
|
||||
'fragment': False,
|
||||
}
|
||||
self.validated_components = self.required_components.copy()
|
||||
|
||||
def allow_schemes(self, *schemes):
|
||||
"""Require the scheme to be one of the provided schemes.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
:param schemes:
|
||||
Schemes, without ``://`` that are allowed.
|
||||
:returns:
|
||||
The validator instance.
|
||||
:rtype:
|
||||
Validator
|
||||
"""
|
||||
for scheme in schemes:
|
||||
self.allowed_schemes.add(normalizers.normalize_scheme(scheme))
|
||||
return self
|
||||
|
||||
def allow_hosts(self, *hosts):
|
||||
"""Require the host to be one of the provided hosts.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
:param hosts:
|
||||
Hosts that are allowed.
|
||||
:returns:
|
||||
The validator instance.
|
||||
:rtype:
|
||||
Validator
|
||||
"""
|
||||
for host in hosts:
|
||||
self.allowed_hosts.add(normalizers.normalize_host(host))
|
||||
return self
|
||||
|
||||
def allow_ports(self, *ports):
|
||||
"""Require the port to be one of the provided ports.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
:param ports:
|
||||
Ports that are allowed.
|
||||
:returns:
|
||||
The validator instance.
|
||||
:rtype:
|
||||
Validator
|
||||
"""
|
||||
for port in ports:
|
||||
port_int = int(port, base=10)
|
||||
if 0 <= port_int <= 65535:
|
||||
self.allowed_ports.add(port)
|
||||
return self
|
||||
|
||||
def allow_use_of_password(self):
|
||||
"""Allow passwords to be present in the URI.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
:returns:
|
||||
The validator instance.
|
||||
:rtype:
|
||||
Validator
|
||||
"""
|
||||
self.allow_password = True
|
||||
return self
|
||||
|
||||
def forbid_use_of_password(self):
|
||||
"""Prevent passwords from being included in the URI.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
:returns:
|
||||
The validator instance.
|
||||
:rtype:
|
||||
Validator
|
||||
"""
|
||||
self.allow_password = False
|
||||
return self
|
||||
|
||||
def check_validity_of(self, *components):
|
||||
"""Check the validity of the components provided.
|
||||
|
||||
This can be specified repeatedly.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
:param components:
|
||||
Names of components from :attr:`Validator.COMPONENT_NAMES`.
|
||||
:returns:
|
||||
The validator instance.
|
||||
:rtype:
|
||||
Validator
|
||||
"""
|
||||
components = [c.lower() for c in components]
|
||||
for component in components:
|
||||
if component not in self.COMPONENT_NAMES:
|
||||
raise ValueError(
|
||||
'"{}" is not a valid component'.format(component)
|
||||
)
|
||||
self.validated_components.update({
|
||||
component: True for component in components
|
||||
})
|
||||
return self
|
||||
|
||||
def require_presence_of(self, *components):
|
||||
"""Require the components provided.
|
||||
|
||||
This can be specified repeatedly.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
:param components:
|
||||
Names of components from :attr:`Validator.COMPONENT_NAMES`.
|
||||
:returns:
|
||||
The validator instance.
|
||||
:rtype:
|
||||
Validator
|
||||
"""
|
||||
components = [c.lower() for c in components]
|
||||
for component in components:
|
||||
if component not in self.COMPONENT_NAMES:
|
||||
raise ValueError(
|
||||
'"{}" is not a valid component'.format(component)
|
||||
)
|
||||
self.required_components.update({
|
||||
component: True for component in components
|
||||
})
|
||||
return self
|
||||
|
||||
def validate(self, uri):
|
||||
"""Check a URI for conditions specified on this validator.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
:param uri:
|
||||
Parsed URI to validate.
|
||||
:type uri:
|
||||
rfc3986.uri.URIReference
|
||||
:raises MissingComponentError:
|
||||
When a required component is missing.
|
||||
:raises UnpermittedComponentError:
|
||||
When a component is not one of those allowed.
|
||||
:raises PasswordForbidden:
|
||||
When a password is present in the userinfo component but is
|
||||
not permitted by configuration.
|
||||
:raises InvalidComponentsError:
|
||||
When a component was found to be invalid.
|
||||
"""
|
||||
if not self.allow_password:
|
||||
check_password(uri)
|
||||
|
||||
required_components = [
|
||||
component
|
||||
for component, required in self.required_components.items()
|
||||
if required
|
||||
]
|
||||
validated_components = [
|
||||
component
|
||||
for component, required in self.validated_components.items()
|
||||
if required
|
||||
]
|
||||
if required_components:
|
||||
ensure_required_components_exist(uri, required_components)
|
||||
if validated_components:
|
||||
ensure_components_are_valid(uri, validated_components)
|
||||
|
||||
ensure_one_of(self.allowed_schemes, uri, 'scheme')
|
||||
ensure_one_of(self.allowed_hosts, uri, 'host')
|
||||
ensure_one_of(self.allowed_ports, uri, 'port')
|
||||
|
||||
|
||||
def check_password(uri):
|
||||
"""Assert that there is no password present in the uri."""
|
||||
userinfo = uri.userinfo
|
||||
if not userinfo:
|
||||
return
|
||||
credentials = userinfo.split(':', 1)
|
||||
if len(credentials) <= 1:
|
||||
return
|
||||
raise exceptions.PasswordForbidden(uri)
|
||||
|
||||
|
||||
def ensure_one_of(allowed_values, uri, attribute):
|
||||
"""Assert that the uri's attribute is one of the allowed values."""
|
||||
value = getattr(uri, attribute)
|
||||
if value is not None and allowed_values and value not in allowed_values:
|
||||
raise exceptions.UnpermittedComponentError(
|
||||
attribute, value, allowed_values,
|
||||
)
|
||||
|
||||
|
||||
def ensure_required_components_exist(uri, required_components):
|
||||
"""Assert that all required components are present in the URI."""
|
||||
missing_components = sorted([
|
||||
component
|
||||
for component in required_components
|
||||
if getattr(uri, component) is None
|
||||
])
|
||||
if missing_components:
|
||||
raise exceptions.MissingComponentError(uri, *missing_components)
|
||||
|
||||
|
||||
def is_valid(value, matcher, require):
|
||||
"""Determine if a value is valid based on the provided matcher.
|
||||
|
||||
:param str value:
|
||||
Value to validate.
|
||||
:param matcher:
|
||||
Compiled regular expression to use to validate the value.
|
||||
:param require:
|
||||
Whether or not the value is required.
|
||||
"""
|
||||
if require:
|
||||
return (value is not None
|
||||
and matcher.match(value))
|
||||
|
||||
# require is False and value is not None
|
||||
return value is None or matcher.match(value)
|
||||
|
||||
|
||||
def authority_is_valid(authority, host=None, require=False):
|
||||
"""Determine if the authority string is valid.
|
||||
|
||||
:param str authority:
|
||||
The authority to validate.
|
||||
:param str host:
|
||||
(optional) The host portion of the authority to validate.
|
||||
:param bool require:
|
||||
(optional) Specify if authority must not be None.
|
||||
:returns:
|
||||
``True`` if valid, ``False`` otherwise
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
validated = is_valid(authority, misc.SUBAUTHORITY_MATCHER, require)
|
||||
if validated and host is not None:
|
||||
return host_is_valid(host, require)
|
||||
return validated
|
||||
|
||||
|
||||
def host_is_valid(host, require=False):
|
||||
"""Determine if the host string is valid.
|
||||
|
||||
:param str host:
|
||||
The host to validate.
|
||||
:param bool require:
|
||||
(optional) Specify if host must not be None.
|
||||
:returns:
|
||||
``True`` if valid, ``False`` otherwise
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
validated = is_valid(host, misc.HOST_MATCHER, require)
|
||||
if validated and host is not None and misc.IPv4_MATCHER.match(host):
|
||||
return valid_ipv4_host_address(host)
|
||||
elif validated and host is not None and misc.IPv6_MATCHER.match(host):
|
||||
return misc.IPv6_NO_RFC4007_MATCHER.match(host) is not None
|
||||
return validated
|
||||
|
||||
|
||||
def scheme_is_valid(scheme, require=False):
|
||||
"""Determine if the scheme is valid.
|
||||
|
||||
:param str scheme:
|
||||
The scheme string to validate.
|
||||
:param bool require:
|
||||
(optional) Set to ``True`` to require the presence of a scheme.
|
||||
:returns:
|
||||
``True`` if the scheme is valid. ``False`` otherwise.
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
return is_valid(scheme, misc.SCHEME_MATCHER, require)
|
||||
|
||||
|
||||
def path_is_valid(path, require=False):
|
||||
"""Determine if the path component is valid.
|
||||
|
||||
:param str path:
|
||||
The path string to validate.
|
||||
:param bool require:
|
||||
(optional) Set to ``True`` to require the presence of a path.
|
||||
:returns:
|
||||
``True`` if the path is valid. ``False`` otherwise.
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
return is_valid(path, misc.PATH_MATCHER, require)
|
||||
|
||||
|
||||
def query_is_valid(query, require=False):
|
||||
"""Determine if the query component is valid.
|
||||
|
||||
:param str query:
|
||||
The query string to validate.
|
||||
:param bool require:
|
||||
(optional) Set to ``True`` to require the presence of a query.
|
||||
:returns:
|
||||
``True`` if the query is valid. ``False`` otherwise.
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
return is_valid(query, misc.QUERY_MATCHER, require)
|
||||
|
||||
|
||||
def fragment_is_valid(fragment, require=False):
|
||||
"""Determine if the fragment component is valid.
|
||||
|
||||
:param str fragment:
|
||||
The fragment string to validate.
|
||||
:param bool require:
|
||||
(optional) Set to ``True`` to require the presence of a fragment.
|
||||
:returns:
|
||||
``True`` if the fragment is valid. ``False`` otherwise.
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
return is_valid(fragment, misc.FRAGMENT_MATCHER, require)
|
||||
|
||||
|
||||
def valid_ipv4_host_address(host):
|
||||
"""Determine if the given host is a valid IPv4 address."""
|
||||
# If the host exists, and it might be IPv4, check each byte in the
|
||||
# address.
|
||||
return all([0 <= int(byte, base=10) <= 255 for byte in host.split('.')])
|
||||
|
||||
|
||||
_COMPONENT_VALIDATORS = {
|
||||
'scheme': scheme_is_valid,
|
||||
'path': path_is_valid,
|
||||
'query': query_is_valid,
|
||||
'fragment': fragment_is_valid,
|
||||
}
|
||||
|
||||
_SUBAUTHORITY_VALIDATORS = set(['userinfo', 'host', 'port'])
|
||||
|
||||
|
||||
def subauthority_component_is_valid(uri, component):
|
||||
"""Determine if the userinfo, host, and port are valid."""
|
||||
try:
|
||||
subauthority_dict = uri.authority_info()
|
||||
except exceptions.InvalidAuthority:
|
||||
return False
|
||||
|
||||
# If we can parse the authority into sub-components and we're not
|
||||
# validating the port, we can assume it's valid.
|
||||
if component == 'host':
|
||||
return host_is_valid(subauthority_dict['host'])
|
||||
elif component != 'port':
|
||||
return True
|
||||
|
||||
try:
|
||||
port = int(subauthority_dict['port'])
|
||||
except TypeError:
|
||||
# If the port wasn't provided it'll be None and int(None) raises a
|
||||
# TypeError
|
||||
return True
|
||||
|
||||
return (0 <= port <= 65535)
|
||||
|
||||
|
||||
def ensure_components_are_valid(uri, validated_components):
|
||||
"""Assert that all components are valid in the URI."""
|
||||
invalid_components = set([])
|
||||
for component in validated_components:
|
||||
if component in _SUBAUTHORITY_VALIDATORS:
|
||||
if not subauthority_component_is_valid(uri, component):
|
||||
invalid_components.add(component)
|
||||
# Python's peephole optimizer means that while this continue *is*
|
||||
# actually executed, coverage.py cannot detect that. See also,
|
||||
# https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered
|
||||
continue # nocov: Python 2.7, 3.3, 3.4
|
||||
|
||||
validator = _COMPONENT_VALIDATORS[component]
|
||||
if not validator(getattr(uri, component)):
|
||||
invalid_components.add(component)
|
||||
|
||||
if invalid_components:
|
||||
raise exceptions.InvalidComponentsError(uri, *invalid_components)
|
||||
Vendored
+9
-4
@@ -7,6 +7,7 @@ from ._collections import RecentlyUsedContainer
|
||||
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool
|
||||
from .connectionpool import port_by_scheme
|
||||
from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown
|
||||
from .packages import six
|
||||
from .packages.six.moves.urllib.parse import urljoin
|
||||
from .request import RequestMethods
|
||||
from .util.url import parse_url
|
||||
@@ -19,7 +20,8 @@ __all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url']
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs',
|
||||
'ssl_version', 'ca_cert_dir', 'ssl_context')
|
||||
'ssl_version', 'ca_cert_dir', 'ssl_context',
|
||||
'key_password')
|
||||
|
||||
# All known keyword arguments that could be provided to the pool manager, its
|
||||
# pools, or the underlying connections. This is used to construct a pool key.
|
||||
@@ -33,6 +35,7 @@ _key_fields = (
|
||||
'key_block', # bool
|
||||
'key_source_address', # str
|
||||
'key_key_file', # str
|
||||
'key_key_password', # str
|
||||
'key_cert_file', # str
|
||||
'key_cert_reqs', # str
|
||||
'key_ca_certs', # str
|
||||
@@ -47,7 +50,7 @@ _key_fields = (
|
||||
'key__socks_options', # dict
|
||||
'key_assert_hostname', # bool or string
|
||||
'key_assert_fingerprint', # str
|
||||
'key_server_hostname', #str
|
||||
'key_server_hostname', # str
|
||||
)
|
||||
|
||||
#: The namedtuple class used to construct keys for the connection pool.
|
||||
@@ -342,8 +345,10 @@ class PoolManager(RequestMethods):
|
||||
# conn.is_same_host() which may use socket.gethostbyname() in the future.
|
||||
if (retries.remove_headers_on_redirect
|
||||
and not conn.is_same_host(redirect_location)):
|
||||
for header in retries.remove_headers_on_redirect:
|
||||
kw['headers'].pop(header, None)
|
||||
headers = list(six.iterkeys(kw['headers']))
|
||||
for header in headers:
|
||||
if header.lower() in retries.remove_headers_on_redirect:
|
||||
kw['headers'].pop(header, None)
|
||||
|
||||
try:
|
||||
retries = retries.increment(method, url, response=response, _pool=conn)
|
||||
|
||||
Vendored
+62
-7
@@ -6,6 +6,11 @@ import logging
|
||||
from socket import timeout as SocketTimeout
|
||||
from socket import error as SocketError
|
||||
|
||||
try:
|
||||
import brotli
|
||||
except ImportError:
|
||||
brotli = None
|
||||
|
||||
from ._collections import HTTPHeaderDict
|
||||
from .exceptions import (
|
||||
BodyNotHttplibCompatible, ProtocolError, DecodeError, ReadTimeoutError,
|
||||
@@ -90,6 +95,25 @@ class GzipDecoder(object):
|
||||
self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
|
||||
|
||||
|
||||
if brotli is not None:
|
||||
class BrotliDecoder(object):
|
||||
# Supports both 'brotlipy' and 'Brotli' packages
|
||||
# since they share an import name. The top branches
|
||||
# are for 'brotlipy' and bottom branches for 'Brotli'
|
||||
def __init__(self):
|
||||
self._obj = brotli.Decompressor()
|
||||
|
||||
def decompress(self, data):
|
||||
if hasattr(self._obj, 'decompress'):
|
||||
return self._obj.decompress(data)
|
||||
return self._obj.process(data)
|
||||
|
||||
def flush(self):
|
||||
if hasattr(self._obj, 'flush'):
|
||||
return self._obj.flush()
|
||||
return b''
|
||||
|
||||
|
||||
class MultiDecoder(object):
|
||||
"""
|
||||
From RFC7231:
|
||||
@@ -118,6 +142,9 @@ def _get_decoder(mode):
|
||||
if mode == 'gzip':
|
||||
return GzipDecoder()
|
||||
|
||||
if brotli is not None and mode == 'br':
|
||||
return BrotliDecoder()
|
||||
|
||||
return DeflateDecoder()
|
||||
|
||||
|
||||
@@ -155,6 +182,8 @@ class HTTPResponse(io.IOBase):
|
||||
"""
|
||||
|
||||
CONTENT_DECODERS = ['gzip', 'deflate']
|
||||
if brotli is not None:
|
||||
CONTENT_DECODERS += ['br']
|
||||
REDIRECT_STATUSES = [301, 302, 303, 307, 308]
|
||||
|
||||
def __init__(self, body='', headers=None, status=0, version=0, reason=None,
|
||||
@@ -311,24 +340,32 @@ class HTTPResponse(io.IOBase):
|
||||
if content_encoding in self.CONTENT_DECODERS:
|
||||
self._decoder = _get_decoder(content_encoding)
|
||||
elif ',' in content_encoding:
|
||||
encodings = [e.strip() for e in content_encoding.split(',') if e.strip() in self.CONTENT_DECODERS]
|
||||
encodings = [
|
||||
e.strip() for e in content_encoding.split(',')
|
||||
if e.strip() in self.CONTENT_DECODERS]
|
||||
if len(encodings):
|
||||
self._decoder = _get_decoder(content_encoding)
|
||||
|
||||
DECODER_ERROR_CLASSES = (IOError, zlib.error)
|
||||
if brotli is not None:
|
||||
DECODER_ERROR_CLASSES += (brotli.error,)
|
||||
|
||||
def _decode(self, data, decode_content, flush_decoder):
|
||||
"""
|
||||
Decode the data passed in and potentially flush the decoder.
|
||||
"""
|
||||
if not decode_content:
|
||||
return data
|
||||
|
||||
try:
|
||||
if decode_content and self._decoder:
|
||||
if self._decoder:
|
||||
data = self._decoder.decompress(data)
|
||||
except (IOError, zlib.error) as e:
|
||||
except self.DECODER_ERROR_CLASSES as e:
|
||||
content_encoding = self.headers.get('content-encoding', '').lower()
|
||||
raise DecodeError(
|
||||
"Received response with content-encoding: %s, but "
|
||||
"failed to decode it." % content_encoding, e)
|
||||
|
||||
if flush_decoder and decode_content:
|
||||
if flush_decoder:
|
||||
data += self._flush_decoder()
|
||||
|
||||
return data
|
||||
@@ -508,9 +545,10 @@ class HTTPResponse(io.IOBase):
|
||||
headers = r.msg
|
||||
|
||||
if not isinstance(headers, HTTPHeaderDict):
|
||||
if PY3: # Python 3
|
||||
if PY3:
|
||||
headers = HTTPHeaderDict(headers.items())
|
||||
else: # Python 2
|
||||
else:
|
||||
# Python 2.7
|
||||
headers = HTTPHeaderDict.from_httplib(headers)
|
||||
|
||||
# HTTPResponse objects in Python 3 don't have a .strict attribute
|
||||
@@ -703,3 +741,20 @@ class HTTPResponse(io.IOBase):
|
||||
return self.retries.history[-1].redirect_location
|
||||
else:
|
||||
return self._request_url
|
||||
|
||||
def __iter__(self):
|
||||
buffer = [b""]
|
||||
for chunk in self.stream(decode_content=True):
|
||||
if b"\n" in chunk:
|
||||
chunk = chunk.split(b"\n")
|
||||
yield b"".join(buffer) + chunk[0] + b"\n"
|
||||
for x in chunk[1:-1]:
|
||||
yield x + b"\n"
|
||||
if chunk[-1]:
|
||||
buffer = [chunk[-1]]
|
||||
else:
|
||||
buffer = []
|
||||
else:
|
||||
buffer.append(chunk)
|
||||
if buffer:
|
||||
yield b"".join(buffer)
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ from .ssl_ import (
|
||||
resolve_cert_reqs,
|
||||
resolve_ssl_version,
|
||||
ssl_wrap_socket,
|
||||
PROTOCOL_TLS,
|
||||
)
|
||||
from .timeout import (
|
||||
current_time,
|
||||
@@ -35,6 +36,7 @@ __all__ = (
|
||||
'IS_PYOPENSSL',
|
||||
'IS_SECURETRANSPORT',
|
||||
'SSLContext',
|
||||
'PROTOCOL_TLS',
|
||||
'Retry',
|
||||
'Timeout',
|
||||
'Url',
|
||||
|
||||
+7
@@ -5,6 +5,13 @@ from ..packages.six import b, integer_types
|
||||
from ..exceptions import UnrewindableBodyError
|
||||
|
||||
ACCEPT_ENCODING = 'gzip,deflate'
|
||||
try:
|
||||
import brotli as _unused_module_brotli # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
ACCEPT_ENCODING += ',br'
|
||||
|
||||
_FAILEDTELL = object()
|
||||
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -179,7 +179,8 @@ class Retry(object):
|
||||
self.raise_on_status = raise_on_status
|
||||
self.history = history or tuple()
|
||||
self.respect_retry_after_header = respect_retry_after_header
|
||||
self.remove_headers_on_redirect = remove_headers_on_redirect
|
||||
self.remove_headers_on_redirect = frozenset([
|
||||
h.lower() for h in remove_headers_on_redirect])
|
||||
|
||||
def new(self, **kw):
|
||||
params = dict(
|
||||
|
||||
Vendored
+61
-50
@@ -2,13 +2,14 @@ from __future__ import absolute_import
|
||||
import errno
|
||||
import warnings
|
||||
import hmac
|
||||
import socket
|
||||
import re
|
||||
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import md5, sha1, sha256
|
||||
|
||||
from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning
|
||||
from ..packages import six
|
||||
from ..packages.rfc3986 import abnf_regexp
|
||||
|
||||
|
||||
SSLContext = None
|
||||
@@ -40,14 +41,33 @@ def _const_compare_digest_backport(a, b):
|
||||
_const_compare_digest = getattr(hmac, 'compare_digest',
|
||||
_const_compare_digest_backport)
|
||||
|
||||
# Borrow rfc3986's regular expressions for IPv4
|
||||
# and IPv6 addresses for use in is_ipaddress()
|
||||
_IP_ADDRESS_REGEX = re.compile(
|
||||
r'^(?:%s|%s|%s)$' % (
|
||||
abnf_regexp.IPv4_RE,
|
||||
abnf_regexp.IPv6_RE,
|
||||
abnf_regexp.IPv6_ADDRZ_RFC4007_RE
|
||||
)
|
||||
)
|
||||
|
||||
try: # Test for SSL features
|
||||
import ssl
|
||||
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
|
||||
from ssl import wrap_socket, CERT_REQUIRED
|
||||
from ssl import HAS_SNI # Has SNI?
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try: # Platform-specific: Python 3.6
|
||||
from ssl import PROTOCOL_TLS
|
||||
PROTOCOL_SSLv23 = PROTOCOL_TLS
|
||||
except ImportError:
|
||||
try:
|
||||
from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS
|
||||
PROTOCOL_SSLv23 = PROTOCOL_TLS
|
||||
except ImportError:
|
||||
PROTOCOL_SSLv23 = PROTOCOL_TLS = 2
|
||||
|
||||
|
||||
try:
|
||||
from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION
|
||||
@@ -56,25 +76,6 @@ except ImportError:
|
||||
OP_NO_COMPRESSION = 0x20000
|
||||
|
||||
|
||||
# Python 2.7 doesn't have inet_pton on non-Linux so we fallback on inet_aton in
|
||||
# those cases. This means that we can only detect IPv4 addresses in this case.
|
||||
if hasattr(socket, 'inet_pton'):
|
||||
inet_pton = socket.inet_pton
|
||||
else:
|
||||
# Maybe we can use ipaddress if the user has urllib3[secure]?
|
||||
try:
|
||||
import ipaddress
|
||||
|
||||
def inet_pton(_, host):
|
||||
if isinstance(host, bytes):
|
||||
host = host.decode('ascii')
|
||||
return ipaddress.ip_address(host)
|
||||
|
||||
except ImportError: # Platform-specific: Non-Linux
|
||||
def inet_pton(_, host):
|
||||
return socket.inet_aton(host)
|
||||
|
||||
|
||||
# A secure default.
|
||||
# Sources for more information on TLS ciphers:
|
||||
#
|
||||
@@ -83,37 +84,35 @@ else:
|
||||
# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
|
||||
#
|
||||
# The general intent is:
|
||||
# - Prefer TLS 1.3 cipher suites
|
||||
# - prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
|
||||
# - prefer ECDHE over DHE for better performance,
|
||||
# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and
|
||||
# security,
|
||||
# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,
|
||||
# - disable NULL authentication, MD5 MACs and DSS for security reasons.
|
||||
# - disable NULL authentication, MD5 MACs, DSS, and other
|
||||
# insecure ciphers for security reasons.
|
||||
# - NOTE: TLS 1.3 cipher suites are managed through a different interface
|
||||
# not exposed by CPython (yet!) and are enabled by default if they're available.
|
||||
DEFAULT_CIPHERS = ':'.join([
|
||||
'TLS13-AES-256-GCM-SHA384',
|
||||
'TLS13-CHACHA20-POLY1305-SHA256',
|
||||
'TLS13-AES-128-GCM-SHA256',
|
||||
'ECDHE+AESGCM',
|
||||
'ECDHE+CHACHA20',
|
||||
'DHE+AESGCM',
|
||||
'DHE+CHACHA20',
|
||||
'ECDH+AESGCM',
|
||||
'ECDH+CHACHA20',
|
||||
'DH+AESGCM',
|
||||
'DH+CHACHA20',
|
||||
'ECDH+AES256',
|
||||
'DH+AES256',
|
||||
'ECDH+AES128',
|
||||
'ECDH+AES',
|
||||
'DH+AES',
|
||||
'RSA+AESGCM',
|
||||
'RSA+AES',
|
||||
'!aNULL',
|
||||
'!eNULL',
|
||||
'!MD5',
|
||||
'!DSS',
|
||||
])
|
||||
|
||||
try:
|
||||
from ssl import SSLContext # Modern SSL?
|
||||
except ImportError:
|
||||
import sys
|
||||
|
||||
class SSLContext(object): # Platform-specific: Python 2
|
||||
def __init__(self, protocol_version):
|
||||
self.protocol = protocol_version
|
||||
@@ -199,7 +198,7 @@ def resolve_cert_reqs(candidate):
|
||||
constant which can directly be passed to wrap_socket.
|
||||
"""
|
||||
if candidate is None:
|
||||
return CERT_NONE
|
||||
return CERT_REQUIRED
|
||||
|
||||
if isinstance(candidate, str):
|
||||
res = getattr(ssl, candidate, None)
|
||||
@@ -215,7 +214,7 @@ def resolve_ssl_version(candidate):
|
||||
like resolve_cert_reqs
|
||||
"""
|
||||
if candidate is None:
|
||||
return PROTOCOL_SSLv23
|
||||
return PROTOCOL_TLS
|
||||
|
||||
if isinstance(candidate, str):
|
||||
res = getattr(ssl, candidate, None)
|
||||
@@ -261,7 +260,7 @@ def create_urllib3_context(ssl_version=None, cert_reqs=None,
|
||||
Constructed SSLContext object with specified options
|
||||
:rtype: SSLContext
|
||||
"""
|
||||
context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23)
|
||||
context = SSLContext(ssl_version or PROTOCOL_TLS)
|
||||
|
||||
context.set_ciphers(ciphers or DEFAULT_CIPHERS)
|
||||
|
||||
@@ -291,7 +290,7 @@ def create_urllib3_context(ssl_version=None, cert_reqs=None,
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None, ciphers=None, ssl_context=None,
|
||||
ca_cert_dir=None):
|
||||
ca_cert_dir=None, key_password=None):
|
||||
"""
|
||||
All arguments except for server_hostname, ssl_context, and ca_cert_dir have
|
||||
the same meaning as they do when using :func:`ssl.wrap_socket`.
|
||||
@@ -307,6 +306,8 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
A directory containing CA certificates in multiple separate files, as
|
||||
supported by OpenSSL's -CApath flag or the capath argument to
|
||||
SSLContext.load_verify_locations().
|
||||
:param key_password:
|
||||
Optional password if the keyfile is encrypted.
|
||||
"""
|
||||
context = ssl_context
|
||||
if context is None:
|
||||
@@ -327,12 +328,22 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
if e.errno == errno.ENOENT:
|
||||
raise SSLError(e)
|
||||
raise
|
||||
elif getattr(context, 'load_default_certs', None) is not None:
|
||||
|
||||
elif ssl_context is None and hasattr(context, 'load_default_certs'):
|
||||
# try to load OS default certs; works well on Windows (require Python3.4+)
|
||||
context.load_default_certs()
|
||||
|
||||
# Attempt to detect if we get the goofy behavior of the
|
||||
# keyfile being encrypted and OpenSSL asking for the
|
||||
# passphrase via the terminal and instead error out.
|
||||
if keyfile and key_password is None and _is_key_file_encrypted(keyfile):
|
||||
raise SSLError("Client private key is encrypted, password is required")
|
||||
|
||||
if certfile:
|
||||
context.load_cert_chain(certfile, keyfile)
|
||||
if key_password is None:
|
||||
context.load_cert_chain(certfile, keyfile)
|
||||
else:
|
||||
context.load_cert_chain(certfile, keyfile, key_password)
|
||||
|
||||
# If we detect server_hostname is an IP address then the SNI
|
||||
# extension should not be used according to RFC3546 Section 3.1
|
||||
@@ -358,7 +369,8 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
|
||||
|
||||
def is_ipaddress(hostname):
|
||||
"""Detects whether the hostname given is an IP address.
|
||||
"""Detects whether the hostname given is an IPv4 or IPv6 address.
|
||||
Also detects IPv6 addresses with Zone IDs.
|
||||
|
||||
:param str hostname: Hostname to examine.
|
||||
:return: True if the hostname is an IP address, False otherwise.
|
||||
@@ -366,16 +378,15 @@ def is_ipaddress(hostname):
|
||||
if six.PY3 and isinstance(hostname, bytes):
|
||||
# IDN A-label bytes are ASCII compatible.
|
||||
hostname = hostname.decode('ascii')
|
||||
return _IP_ADDRESS_REGEX.match(hostname) is not None
|
||||
|
||||
families = [socket.AF_INET]
|
||||
if hasattr(socket, 'AF_INET6'):
|
||||
families.append(socket.AF_INET6)
|
||||
|
||||
for af in families:
|
||||
try:
|
||||
inet_pton(af, hostname)
|
||||
except (socket.error, ValueError, OSError):
|
||||
pass
|
||||
else:
|
||||
return True
|
||||
def _is_key_file_encrypted(key_file):
|
||||
"""Detects if a key file is encrypted or not."""
|
||||
with open(key_file, 'r') as f:
|
||||
for line in f:
|
||||
# Look for Proc-Type: 4,ENCRYPTED
|
||||
if 'ENCRYPTED' in line:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+2
-1
@@ -131,7 +131,8 @@ class Timeout(object):
|
||||
raise ValueError("Attempted to set %s timeout to %s, but the "
|
||||
"timeout cannot be set to a value less "
|
||||
"than or equal to 0." % (name, value))
|
||||
except TypeError: # Python 3
|
||||
except TypeError:
|
||||
# Python 3
|
||||
raise ValueError("Timeout value %s was %s, but it must be an "
|
||||
"int, float or None." % (name, value))
|
||||
|
||||
|
||||
Vendored
+132
-73
@@ -1,7 +1,12 @@
|
||||
from __future__ import absolute_import
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
from ..exceptions import LocationParseError
|
||||
from ..packages import six, rfc3986
|
||||
from ..packages.rfc3986.exceptions import RFC3986Exception, ValidationError
|
||||
from ..packages.rfc3986.validators import Validator
|
||||
from ..packages.rfc3986 import abnf_regexp, normalizers, compat, misc
|
||||
|
||||
|
||||
url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment']
|
||||
@@ -10,10 +15,16 @@ url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment']
|
||||
# urllib3 infers URLs without a scheme (None) to be http.
|
||||
NORMALIZABLE_SCHEMES = ('http', 'https', None)
|
||||
|
||||
# Regex for detecting URLs with schemes. RFC 3986 Section 3.1
|
||||
SCHEME_REGEX = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+\-]*:|/)")
|
||||
|
||||
PATH_CHARS = abnf_regexp.UNRESERVED_CHARS_SET | abnf_regexp.SUB_DELIMITERS_SET | {':', '@', '/'}
|
||||
QUERY_CHARS = FRAGMENT_CHARS = PATH_CHARS | {'?'}
|
||||
|
||||
|
||||
class Url(namedtuple('Url', url_attrs)):
|
||||
"""
|
||||
Datastructure for representing an HTTP URL. Used as a return value for
|
||||
Data structure for representing an HTTP URL. Used as a return value for
|
||||
:func:`parse_url`. Both the scheme and host are normalized as they are
|
||||
both case-insensitive according to RFC 3986.
|
||||
"""
|
||||
@@ -23,10 +34,8 @@ class Url(namedtuple('Url', url_attrs)):
|
||||
query=None, fragment=None):
|
||||
if path and not path.startswith('/'):
|
||||
path = '/' + path
|
||||
if scheme:
|
||||
if scheme is not None:
|
||||
scheme = scheme.lower()
|
||||
if host and scheme in NORMALIZABLE_SCHEMES:
|
||||
host = host.lower()
|
||||
return super(Url, cls).__new__(cls, scheme, auth, host, port, path,
|
||||
query, fragment)
|
||||
|
||||
@@ -72,23 +81,23 @@ class Url(namedtuple('Url', url_attrs)):
|
||||
'http://username:password@host.com:80/path?query#fragment'
|
||||
"""
|
||||
scheme, auth, host, port, path, query, fragment = self
|
||||
url = ''
|
||||
url = u''
|
||||
|
||||
# We use "is not None" we want things to happen with empty strings (or 0 port)
|
||||
if scheme is not None:
|
||||
url += scheme + '://'
|
||||
url += scheme + u'://'
|
||||
if auth is not None:
|
||||
url += auth + '@'
|
||||
url += auth + u'@'
|
||||
if host is not None:
|
||||
url += host
|
||||
if port is not None:
|
||||
url += ':' + str(port)
|
||||
url += u':' + str(port)
|
||||
if path is not None:
|
||||
url += path
|
||||
if query is not None:
|
||||
url += '?' + query
|
||||
url += u'?' + query
|
||||
if fragment is not None:
|
||||
url += '#' + fragment
|
||||
url += u'#' + fragment
|
||||
|
||||
return url
|
||||
|
||||
@@ -98,6 +107,8 @@ class Url(namedtuple('Url', url_attrs)):
|
||||
|
||||
def split_first(s, delims):
|
||||
"""
|
||||
.. deprecated:: 1.25
|
||||
|
||||
Given a string and an iterable of delimiters, split on the first found
|
||||
delimiter. Return two split parts and the matched delimiter.
|
||||
|
||||
@@ -129,10 +140,44 @@ def split_first(s, delims):
|
||||
return s[:min_idx], s[min_idx + 1:], min_delim
|
||||
|
||||
|
||||
def _encode_invalid_chars(component, allowed_chars, encoding='utf-8'):
|
||||
"""Percent-encodes a URI component without reapplying
|
||||
onto an already percent-encoded component. Based on
|
||||
rfc3986.normalizers.encode_component()
|
||||
"""
|
||||
if component is None:
|
||||
return component
|
||||
|
||||
# Try to see if the component we're encoding is already percent-encoded
|
||||
# so we can skip all '%' characters but still encode all others.
|
||||
percent_encodings = len(normalizers.PERCENT_MATCHER.findall(
|
||||
compat.to_str(component, encoding)))
|
||||
|
||||
uri_bytes = component.encode('utf-8', 'surrogatepass')
|
||||
is_percent_encoded = percent_encodings == uri_bytes.count(b'%')
|
||||
|
||||
encoded_component = bytearray()
|
||||
|
||||
for i in range(0, len(uri_bytes)):
|
||||
# Will return a single character bytestring on both Python 2 & 3
|
||||
byte = uri_bytes[i:i+1]
|
||||
byte_ord = ord(byte)
|
||||
if ((is_percent_encoded and byte == b'%')
|
||||
or (byte_ord < 128 and byte.decode() in allowed_chars)):
|
||||
encoded_component.extend(byte)
|
||||
continue
|
||||
encoded_component.extend('%{0:02x}'.format(byte_ord).encode().upper())
|
||||
|
||||
return encoded_component.decode(encoding)
|
||||
|
||||
|
||||
def parse_url(url):
|
||||
"""
|
||||
Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
|
||||
performed to parse incomplete urls. Fields not provided will be None.
|
||||
This parser is RFC 3986 compliant.
|
||||
|
||||
:param str url: URL to parse into a :class:`.Url` namedtuple.
|
||||
|
||||
Partly backwards-compatible with :mod:`urlparse`.
|
||||
|
||||
@@ -145,81 +190,95 @@ def parse_url(url):
|
||||
>>> parse_url('/foo?bar')
|
||||
Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)
|
||||
"""
|
||||
|
||||
# While this code has overlap with stdlib's urlparse, it is much
|
||||
# simplified for our needs and less annoying.
|
||||
# Additionally, this implementations does silly things to be optimal
|
||||
# on CPython.
|
||||
|
||||
if not url:
|
||||
# Empty
|
||||
return Url()
|
||||
|
||||
scheme = None
|
||||
auth = None
|
||||
host = None
|
||||
port = None
|
||||
path = None
|
||||
fragment = None
|
||||
query = None
|
||||
is_string = not isinstance(url, six.binary_type)
|
||||
|
||||
# Scheme
|
||||
if '://' in url:
|
||||
scheme, url = url.split('://', 1)
|
||||
# RFC 3986 doesn't like URLs that have a host but don't start
|
||||
# with a scheme and we support URLs like that so we need to
|
||||
# detect that problem and add an empty scheme indication.
|
||||
# We don't get hurt on path-only URLs here as it's stripped
|
||||
# off and given an empty scheme anyways.
|
||||
if not SCHEME_REGEX.search(url):
|
||||
url = "//" + url
|
||||
|
||||
# Find the earliest Authority Terminator
|
||||
# (http://tools.ietf.org/html/rfc3986#section-3.2)
|
||||
url, path_, delim = split_first(url, ['/', '?', '#'])
|
||||
|
||||
if delim:
|
||||
# Reassemble the path
|
||||
path = delim + path_
|
||||
|
||||
# Auth
|
||||
if '@' in url:
|
||||
# Last '@' denotes end of auth part
|
||||
auth, url = url.rsplit('@', 1)
|
||||
|
||||
# IPv6
|
||||
if url and url[0] == '[':
|
||||
host, url = url.split(']', 1)
|
||||
host += ']'
|
||||
|
||||
# Port
|
||||
if ':' in url:
|
||||
_host, port = url.split(':', 1)
|
||||
|
||||
if not host:
|
||||
host = _host
|
||||
|
||||
if port:
|
||||
# If given, ports must be integers. No whitespace, no plus or
|
||||
# minus prefixes, no non-integer digits such as ^2 (superscript).
|
||||
if not port.isdigit():
|
||||
raise LocationParseError(url)
|
||||
def idna_encode(name):
|
||||
if name and any([ord(x) > 128 for x in name]):
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
raise LocationParseError(url)
|
||||
else:
|
||||
# Blank ports are cool, too. (rfc3986#section-3.2.3)
|
||||
port = None
|
||||
import idna
|
||||
except ImportError:
|
||||
raise LocationParseError("Unable to parse URL without the 'idna' module")
|
||||
try:
|
||||
return idna.encode(name.lower(), strict=True, std3_rules=True)
|
||||
except idna.IDNAError:
|
||||
raise LocationParseError(u"Name '%s' is not a valid IDNA label" % name)
|
||||
return name
|
||||
|
||||
elif not host and url:
|
||||
host = url
|
||||
try:
|
||||
split_iri = misc.IRI_MATCHER.match(compat.to_str(url)).groupdict()
|
||||
iri_ref = rfc3986.IRIReference(
|
||||
split_iri['scheme'], split_iri['authority'],
|
||||
_encode_invalid_chars(split_iri['path'], PATH_CHARS),
|
||||
_encode_invalid_chars(split_iri['query'], QUERY_CHARS),
|
||||
_encode_invalid_chars(split_iri['fragment'], FRAGMENT_CHARS)
|
||||
)
|
||||
has_authority = iri_ref.authority is not None
|
||||
uri_ref = iri_ref.encode(idna_encoder=idna_encode)
|
||||
except (ValueError, RFC3986Exception):
|
||||
return six.raise_from(LocationParseError(url), None)
|
||||
|
||||
# rfc3986 strips the authority if it's invalid
|
||||
if has_authority and uri_ref.authority is None:
|
||||
raise LocationParseError(url)
|
||||
|
||||
# Only normalize schemes we understand to not break http+unix
|
||||
# or other schemes that don't follow RFC 3986.
|
||||
if uri_ref.scheme is None or uri_ref.scheme.lower() in NORMALIZABLE_SCHEMES:
|
||||
uri_ref = uri_ref.normalize()
|
||||
|
||||
# Validate all URIReference components and ensure that all
|
||||
# components that were set before are still set after
|
||||
# normalization has completed.
|
||||
validator = Validator()
|
||||
try:
|
||||
validator.check_validity_of(
|
||||
*validator.COMPONENT_NAMES
|
||||
).validate(uri_ref)
|
||||
except ValidationError:
|
||||
return six.raise_from(LocationParseError(url), None)
|
||||
|
||||
# For the sake of backwards compatibility we put empty
|
||||
# string values for path if there are any defined values
|
||||
# beyond the path in the URL.
|
||||
# TODO: Remove this when we break backwards compatibility.
|
||||
path = uri_ref.path
|
||||
if not path:
|
||||
return Url(scheme, auth, host, port, path, query, fragment)
|
||||
if (uri_ref.query is not None
|
||||
or uri_ref.fragment is not None):
|
||||
path = ""
|
||||
else:
|
||||
path = None
|
||||
|
||||
# Fragment
|
||||
if '#' in path:
|
||||
path, fragment = path.split('#', 1)
|
||||
# Ensure that each part of the URL is a `str` for
|
||||
# backwards compatibility.
|
||||
def to_input_type(x):
|
||||
if x is None:
|
||||
return None
|
||||
elif not is_string and not isinstance(x, six.binary_type):
|
||||
return x.encode('utf-8')
|
||||
return x
|
||||
|
||||
# Query
|
||||
if '?' in path:
|
||||
path, query = path.split('?', 1)
|
||||
|
||||
return Url(scheme, auth, host, port, path, query, fragment)
|
||||
return Url(
|
||||
scheme=to_input_type(uri_ref.scheme),
|
||||
auth=to_input_type(uri_ref.userinfo),
|
||||
host=to_input_type(uri_ref.host),
|
||||
port=int(uri_ref.port) if uri_ref.port is not None else None,
|
||||
path=to_input_type(path),
|
||||
query=to_input_type(uri_ref.query),
|
||||
fragment=to_input_type(uri_ref.fragment)
|
||||
)
|
||||
|
||||
|
||||
def get_host(url):
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from .functools import partialmethod
|
||||
from .surrogateescape import register_surrogateescape
|
||||
from .tempfile import NamedTemporaryFile
|
||||
|
||||
__all__ = ["NamedTemporaryFile", "partialmethod"]
|
||||
__all__ = ["NamedTemporaryFile", "partialmethod", "register_surrogateescape"]
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
This is Victor Stinner's pure-Python implementation of PEP 383: the "surrogateescape" error
|
||||
handler of Python 3.
|
||||
Source: misc/python/surrogateescape.py in https://bitbucket.org/haypo/misc
|
||||
"""
|
||||
|
||||
# This code is released under the Python license and the BSD 2-clause license
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
FS_ERRORS = "surrogateescape"
|
||||
|
||||
# # -- Python 2/3 compatibility -------------------------------------
|
||||
# FS_ERRORS = 'my_surrogateescape'
|
||||
|
||||
|
||||
def u(text):
|
||||
if six.PY3:
|
||||
return text
|
||||
else:
|
||||
return text.decode("unicode_escape")
|
||||
|
||||
|
||||
def b(data):
|
||||
if six.PY3:
|
||||
return data.encode("latin1")
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
if six.PY3:
|
||||
_unichr = chr
|
||||
bytes_chr = lambda code: bytes((code,))
|
||||
else:
|
||||
_unichr = unichr
|
||||
bytes_chr = chr
|
||||
|
||||
|
||||
def surrogateescape_handler(exc):
|
||||
"""
|
||||
Pure Python implementation of the PEP 383: the "surrogateescape" error
|
||||
handler of Python 3. Undecodable bytes will be replaced by a Unicode
|
||||
character U+DCxx on decoding, and these are translated into the
|
||||
original bytes on encoding.
|
||||
"""
|
||||
mystring = exc.object[exc.start : exc.end]
|
||||
|
||||
try:
|
||||
if isinstance(exc, UnicodeDecodeError):
|
||||
# mystring is a byte-string in this case
|
||||
decoded = replace_surrogate_decode(mystring)
|
||||
elif isinstance(exc, UnicodeEncodeError):
|
||||
# In the case of u'\udcc3'.encode('ascii',
|
||||
# 'this_surrogateescape_handler'), both Python 2.x and 3.x raise an
|
||||
# exception anyway after this function is called, even though I think
|
||||
# it's doing what it should. It seems that the strict encoder is called
|
||||
# to encode the unicode string that this function returns ...
|
||||
decoded = replace_surrogate_encode(mystring)
|
||||
else:
|
||||
raise exc
|
||||
except NotASurrogateError:
|
||||
raise exc
|
||||
return (decoded, exc.end)
|
||||
|
||||
|
||||
class NotASurrogateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def replace_surrogate_encode(mystring):
|
||||
"""
|
||||
Returns a (unicode) string, not the more logical bytes, because the codecs
|
||||
register_error functionality expects this.
|
||||
"""
|
||||
decoded = []
|
||||
for ch in mystring:
|
||||
# if utils.PY3:
|
||||
# code = ch
|
||||
# else:
|
||||
code = ord(ch)
|
||||
|
||||
# The following magic comes from Py3.3's Python/codecs.c file:
|
||||
if not 0xD800 <= code <= 0xDCFF:
|
||||
# Not a surrogate. Fail with the original exception.
|
||||
raise NotASurrogateError
|
||||
# mybytes = [0xe0 | (code >> 12),
|
||||
# 0x80 | ((code >> 6) & 0x3f),
|
||||
# 0x80 | (code & 0x3f)]
|
||||
# Is this a good idea?
|
||||
if 0xDC00 <= code <= 0xDC7F:
|
||||
decoded.append(_unichr(code - 0xDC00))
|
||||
elif code <= 0xDCFF:
|
||||
decoded.append(_unichr(code - 0xDC00))
|
||||
else:
|
||||
raise NotASurrogateError
|
||||
return str().join(decoded)
|
||||
|
||||
|
||||
def replace_surrogate_decode(mybytes):
|
||||
"""
|
||||
Returns a (unicode) string
|
||||
"""
|
||||
decoded = []
|
||||
for ch in mybytes:
|
||||
# We may be parsing newbytes (in which case ch is an int) or a native
|
||||
# str on Py2
|
||||
if isinstance(ch, int):
|
||||
code = ch
|
||||
else:
|
||||
code = ord(ch)
|
||||
if 0x80 <= code <= 0xFF:
|
||||
decoded.append(_unichr(0xDC00 + code))
|
||||
elif code <= 0x7F:
|
||||
decoded.append(_unichr(code))
|
||||
else:
|
||||
# # It may be a bad byte
|
||||
# # Try swallowing it.
|
||||
# continue
|
||||
# print("RAISE!")
|
||||
raise NotASurrogateError
|
||||
return str().join(decoded)
|
||||
|
||||
|
||||
def encodefilename(fn):
|
||||
if FS_ENCODING == "ascii":
|
||||
# ASCII encoder of Python 2 expects that the error handler returns a
|
||||
# Unicode string encodable to ASCII, whereas our surrogateescape error
|
||||
# handler has to return bytes in 0x80-0xFF range.
|
||||
encoded = []
|
||||
for index, ch in enumerate(fn):
|
||||
code = ord(ch)
|
||||
if code < 128:
|
||||
ch = bytes_chr(code)
|
||||
elif 0xDC80 <= code <= 0xDCFF:
|
||||
ch = bytes_chr(code - 0xDC00)
|
||||
else:
|
||||
raise UnicodeEncodeError(
|
||||
FS_ENCODING, fn, index, index + 1, "ordinal not in range(128)"
|
||||
)
|
||||
encoded.append(ch)
|
||||
return bytes().join(encoded)
|
||||
elif FS_ENCODING == "utf-8":
|
||||
# UTF-8 encoder of Python 2 encodes surrogates, so U+DC80-U+DCFF
|
||||
# doesn't go through our error handler
|
||||
encoded = []
|
||||
for index, ch in enumerate(fn):
|
||||
code = ord(ch)
|
||||
if 0xD800 <= code <= 0xDFFF:
|
||||
if 0xDC80 <= code <= 0xDCFF:
|
||||
ch = bytes_chr(code - 0xDC00)
|
||||
encoded.append(ch)
|
||||
else:
|
||||
raise UnicodeEncodeError(
|
||||
FS_ENCODING, fn, index, index + 1, "surrogates not allowed"
|
||||
)
|
||||
else:
|
||||
ch_utf8 = ch.encode("utf-8")
|
||||
encoded.append(ch_utf8)
|
||||
return bytes().join(encoded)
|
||||
else:
|
||||
return fn.encode(FS_ENCODING, FS_ERRORS)
|
||||
|
||||
|
||||
def decodefilename(fn):
|
||||
return fn.decode(FS_ENCODING, FS_ERRORS)
|
||||
|
||||
|
||||
FS_ENCODING = "ascii"
|
||||
fn = b("[abc\xff]")
|
||||
encoded = u("[abc\udcff]")
|
||||
# FS_ENCODING = 'cp932'; fn = b('[abc\x81\x00]'); encoded = u('[abc\udc81\x00]')
|
||||
# FS_ENCODING = 'UTF-8'; fn = b('[abc\xff]'); encoded = u('[abc\udcff]')
|
||||
|
||||
|
||||
# normalize the filesystem encoding name.
|
||||
# For example, we expect "utf-8", not "UTF8".
|
||||
FS_ENCODING = codecs.lookup(FS_ENCODING).name
|
||||
|
||||
|
||||
def register_surrogateescape():
|
||||
"""
|
||||
Registers the surrogateescape error handler on Python 2 (only)
|
||||
"""
|
||||
if six.PY3:
|
||||
return
|
||||
try:
|
||||
codecs.lookup_error(FS_ERRORS)
|
||||
except LookupError:
|
||||
codecs.register_error(FS_ERRORS, surrogateescape_handler)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
+19
-1
@@ -15,6 +15,24 @@ except ImportError:
|
||||
from pipenv.vendor.backports.weakref import finalize
|
||||
|
||||
|
||||
def fs_encode(path):
|
||||
try:
|
||||
return os.fsencode(path)
|
||||
except AttributeError:
|
||||
from ..compat import fs_encode
|
||||
|
||||
return fs_encode(path)
|
||||
|
||||
|
||||
def fs_decode(path):
|
||||
try:
|
||||
return os.fsdecode(path)
|
||||
except AttributeError:
|
||||
from ..compat import fs_decode
|
||||
|
||||
return fs_decode(path)
|
||||
|
||||
|
||||
__all__ = ["finalize", "NamedTemporaryFile"]
|
||||
|
||||
|
||||
@@ -48,7 +66,7 @@ def _sanitize_params(prefix, suffix, dir):
|
||||
if output_type is str:
|
||||
dir = gettempdir()
|
||||
else:
|
||||
dir = os.fsencode(gettempdir())
|
||||
dir = fs_encode(gettempdir())
|
||||
return prefix, suffix, dir, output_type
|
||||
|
||||
|
||||
|
||||
Vendored
+19
-17
@@ -40,35 +40,35 @@ __all__ = [
|
||||
"_fs_decode_errors",
|
||||
]
|
||||
|
||||
if sys.version_info >= (3, 5):
|
||||
if sys.version_info >= (3, 5): # pragma: no cover
|
||||
from pathlib import Path
|
||||
else:
|
||||
from pathlib2 import Path
|
||||
else: # pragma: no cover
|
||||
from pipenv.vendor.pathlib2 import Path
|
||||
|
||||
if six.PY3:
|
||||
if six.PY3: # pragma: no cover
|
||||
# Only Python 3.4+ is supported
|
||||
from functools import lru_cache, partialmethod
|
||||
from tempfile import NamedTemporaryFile
|
||||
from shutil import get_terminal_size
|
||||
from weakref import finalize
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
# Only Python 2.7 is supported
|
||||
from backports.functools_lru_cache import lru_cache
|
||||
from pipenv.vendor.backports.functools_lru_cache import lru_cache
|
||||
from .backports.functools import partialmethod # type: ignore
|
||||
from backports.shutil_get_terminal_size import get_terminal_size
|
||||
from pipenv.vendor.backports.shutil_get_terminal_size import get_terminal_size
|
||||
from .backports.surrogateescape import register_surrogateescape
|
||||
|
||||
register_surrogateescape()
|
||||
NamedTemporaryFile = _NamedTemporaryFile
|
||||
from backports.weakref import finalize # type: ignore
|
||||
from pipenv.vendor.backports.weakref import finalize # type: ignore
|
||||
|
||||
try:
|
||||
# Introduced Python 3.5
|
||||
from json import JSONDecodeError
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
JSONDecodeError = ValueError # type: ignore
|
||||
|
||||
if six.PY2:
|
||||
if six.PY2: # pragma: no cover
|
||||
|
||||
from io import BytesIO as StringIO
|
||||
|
||||
@@ -98,7 +98,7 @@ if six.PY2:
|
||||
super(FileExistsError, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
from builtins import (
|
||||
ResourceWarning,
|
||||
FileNotFoundError,
|
||||
@@ -139,7 +139,7 @@ def is_type_checking():
|
||||
return TYPE_CHECKING
|
||||
|
||||
|
||||
IS_TYPE_CHECKING = is_type_checking()
|
||||
IS_TYPE_CHECKING = os.environ.get("MYPY_RUNNING", is_type_checking())
|
||||
|
||||
|
||||
class TemporaryDirectory(object):
|
||||
@@ -351,23 +351,25 @@ def fs_decode(path):
|
||||
if path is None:
|
||||
raise TypeError("expected a valid path to decode")
|
||||
if isinstance(path, six.binary_type):
|
||||
if six.PY2:
|
||||
from array import array
|
||||
import array
|
||||
|
||||
indexes = _invalid_utf8_indexes(array(str("B"), path))
|
||||
indexes = _invalid_utf8_indexes(array.array(str("B"), path))
|
||||
if six.PY2:
|
||||
return "".join(
|
||||
chunk.decode(_fs_encoding, _fs_decode_errors)
|
||||
for chunk in _chunks(path, indexes)
|
||||
)
|
||||
if indexes and os.name == "nt":
|
||||
return path.decode(_fs_encoding, "surrogateescape")
|
||||
return path.decode(_fs_encoding, _fs_decode_errors)
|
||||
return path
|
||||
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
if sys.version_info[0] < 3: # pragma: no cover
|
||||
_fs_encode_errors = "surrogateescape"
|
||||
_fs_decode_errors = "surrogateescape"
|
||||
_fs_encoding = "utf-8"
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
_fs_encoding = "utf-8"
|
||||
if sys.platform.startswith("win"):
|
||||
_fs_error_fn = None
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
from .compat import IS_TYPE_CHECKING
|
||||
|
||||
MYPY_RUNNING = IS_TYPE_CHECKING
|
||||
Vendored
+60
-31
@@ -26,6 +26,7 @@ from .compat import (
|
||||
to_native_string,
|
||||
)
|
||||
from .contextmanagers import spinner as spinner
|
||||
from .environment import MYPY_RUNNING
|
||||
from .termcolors import ANSI_REMOVAL_RE, colorize
|
||||
|
||||
if os.name != "nt":
|
||||
@@ -55,7 +56,13 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
if MYPY_RUNNING:
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from .spin import VistirSpinner
|
||||
|
||||
|
||||
def _get_logger(name=None, level="ERROR"):
|
||||
# type: (Optional[str], str) -> logging.Logger
|
||||
if not name:
|
||||
name = __name__
|
||||
if isinstance(level, six.string_types):
|
||||
@@ -72,6 +79,7 @@ def _get_logger(name=None, level="ERROR"):
|
||||
|
||||
|
||||
def shell_escape(cmd):
|
||||
# type: (Union[str, List[str]]) -> str
|
||||
"""Escape strings for use in :func:`~subprocess.Popen` and :func:`run`.
|
||||
|
||||
This is a passthrough method for instantiating a :class:`~vistir.cmdparse.Script`
|
||||
@@ -82,6 +90,7 @@ def shell_escape(cmd):
|
||||
|
||||
|
||||
def unnest(elem):
|
||||
# type: (Iterable) -> Any
|
||||
"""Flatten an arbitrarily nested iterable
|
||||
|
||||
:param elem: An iterable to flatten
|
||||
@@ -96,22 +105,27 @@ def unnest(elem):
|
||||
elem, target = tee(elem, 2)
|
||||
else:
|
||||
target = elem
|
||||
for el in target:
|
||||
if isinstance(el, Iterable) and not isinstance(el, six.string_types):
|
||||
el, el_copy = tee(el, 2)
|
||||
for sub in unnest(el_copy):
|
||||
yield sub
|
||||
else:
|
||||
yield el
|
||||
if not target or not _is_iterable(target):
|
||||
yield target
|
||||
else:
|
||||
for el in target:
|
||||
if isinstance(el, Iterable) and not isinstance(el, six.string_types):
|
||||
el, el_copy = tee(el, 2)
|
||||
for sub in unnest(el_copy):
|
||||
yield sub
|
||||
else:
|
||||
yield el
|
||||
|
||||
|
||||
def _is_iterable(elem):
|
||||
if getattr(elem, "__iter__", False):
|
||||
# type: (Any) -> bool
|
||||
if getattr(elem, "__iter__", False) or isinstance(elem, Iterable):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def dedup(iterable):
|
||||
# type: (Iterable) -> Iterable
|
||||
"""Deduplicate an iterable object like iter(set(iterable)) but
|
||||
order-reserved.
|
||||
"""
|
||||
@@ -119,6 +133,7 @@ def dedup(iterable):
|
||||
|
||||
|
||||
def _spawn_subprocess(script, env=None, block=True, cwd=None, combine_stderr=True):
|
||||
# type: (Union[str, List[str]], Optional[Dict[str, str], bool, Optional[str], bool]) -> subprocess.Popen
|
||||
from distutils.spawn import find_executable
|
||||
|
||||
if not env:
|
||||
@@ -146,7 +161,7 @@ def _spawn_subprocess(script, env=None, block=True, cwd=None, combine_stderr=Tru
|
||||
# a "command" that is non-executable. See pypa/pipenv#2727.
|
||||
try:
|
||||
return subprocess.Popen(cmd, **options)
|
||||
except WindowsError as e:
|
||||
except WindowsError as e: # pragma: no cover
|
||||
if getattr(e, "winerror", 9999) != 193:
|
||||
raise
|
||||
options["shell"] = True
|
||||
@@ -203,6 +218,25 @@ def get_stream_results(cmd_instance, verbose, maxlen, spinner=None, stdout_allow
|
||||
return stream_results
|
||||
|
||||
|
||||
def _handle_nonblocking_subprocess(c, spinner=None):
|
||||
# type: (subprocess.Popen, VistirSpinner) -> subprocess.Popen
|
||||
try:
|
||||
c.wait()
|
||||
finally:
|
||||
if c.stdout:
|
||||
c.stdout.close()
|
||||
if c.stderr:
|
||||
c.stderr.close()
|
||||
if spinner:
|
||||
if c.returncode > 0:
|
||||
spinner.fail(to_native_string("Failed...cleaning up..."))
|
||||
if not os.name == "nt":
|
||||
spinner.ok(to_native_string("✔ Complete"))
|
||||
else:
|
||||
spinner.ok(to_native_string("Complete"))
|
||||
return c
|
||||
|
||||
|
||||
def _create_subprocess(
|
||||
cmd,
|
||||
env=None,
|
||||
@@ -225,9 +259,12 @@ def _create_subprocess(
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
formatted_tb = "".join(traceback.format_exception(*sys.exc_info())) # pragma: no cover
|
||||
formatted_tb = "".join(
|
||||
traceback.format_exception(*sys.exc_info())
|
||||
) # pragma: no cover
|
||||
sys.stderr.write( # pragma: no cover
|
||||
"Error while executing command %s:" % to_native_string(" ".join(cmd._parts)) # pragma: no cover
|
||||
"Error while executing command %s:"
|
||||
% to_native_string(" ".join(cmd._parts)) # pragma: no cover
|
||||
) # pragma: no cover
|
||||
sys.stderr.write(formatted_tb) # pragma: no cover
|
||||
raise exc # pragma: no cover
|
||||
@@ -245,26 +282,17 @@ def _create_subprocess(
|
||||
spinner=spinner,
|
||||
stdout_allowed=write_to_stdout,
|
||||
)
|
||||
try:
|
||||
c.wait()
|
||||
finally:
|
||||
if c.stdout:
|
||||
c.stdout.close()
|
||||
if c.stderr:
|
||||
c.stderr.close()
|
||||
if spinner:
|
||||
if c.returncode > 0:
|
||||
spinner.fail(to_native_string("Failed...cleaning up..."))
|
||||
if not os.name == "nt":
|
||||
spinner.ok(to_native_string("✔ Complete"))
|
||||
else:
|
||||
spinner.ok(to_native_string("Complete"))
|
||||
_handle_nonblocking_subprocess(c, spinner)
|
||||
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:
|
||||
c.out, c.err = c.communicate()
|
||||
try:
|
||||
c.out, c.err = c.communicate()
|
||||
except (SystemExit, TimeoutError):
|
||||
c.terminate()
|
||||
c.out, c.err = c.communicate()
|
||||
if not block:
|
||||
c.wait()
|
||||
c.out = to_text("{0}".format(c.out)) if c.out else fs_str("")
|
||||
@@ -432,8 +460,8 @@ def to_bytes(string, encoding="utf-8", errors=None):
|
||||
else:
|
||||
return string.decode(unicode_name).encode(encoding, errors)
|
||||
elif isinstance(string, memoryview):
|
||||
return bytes(string)
|
||||
elif not isinstance(string, six.string_types):
|
||||
return string.tobytes()
|
||||
elif not isinstance(string, six.string_types): # pragma: no cover
|
||||
try:
|
||||
if six.PY3:
|
||||
return six.text_type(string).encode(encoding, errors)
|
||||
@@ -476,13 +504,13 @@ def to_text(string, encoding="utf-8", errors=None):
|
||||
string = six.text_type(string, encoding, errors)
|
||||
else:
|
||||
string = six.text_type(string)
|
||||
elif hasattr(string, "__unicode__"):
|
||||
elif hasattr(string, "__unicode__"): # pragma: no cover
|
||||
string = six.text_type(string)
|
||||
else:
|
||||
string = six.text_type(bytes(string), encoding, errors)
|
||||
else:
|
||||
string = string.decode(encoding, errors)
|
||||
except UnicodeDecodeError:
|
||||
except UnicodeDecodeError: # pragma: no cover
|
||||
string = " ".join(to_text(arg, encoding, errors) for arg in string)
|
||||
return string
|
||||
|
||||
@@ -795,7 +823,7 @@ class _StreamProvider(object):
|
||||
def _isatty(stream):
|
||||
try:
|
||||
is_a_tty = stream.isatty()
|
||||
except Exception:
|
||||
except Exception: # pragma: no cover
|
||||
is_a_tty = False
|
||||
return is_a_tty
|
||||
|
||||
@@ -812,6 +840,7 @@ _color_stream_cache = WeakKeyDictionary()
|
||||
if os.name == "nt" or sys.platform.startswith("win"):
|
||||
|
||||
if colorama is not None:
|
||||
|
||||
def _wrap_for_color(stream, color=None):
|
||||
try:
|
||||
cached = _color_stream_cache.get(stream)
|
||||
|
||||
Vendored
+22
-6
@@ -17,17 +17,17 @@ from six.moves.urllib import request as urllib_request
|
||||
|
||||
from .backports.tempfile import _TemporaryFileWrapper
|
||||
from .compat import (
|
||||
IS_TYPE_CHECKING,
|
||||
FileNotFoundError,
|
||||
Path,
|
||||
PermissionError,
|
||||
ResourceWarning,
|
||||
TemporaryDirectory,
|
||||
FileNotFoundError,
|
||||
PermissionError,
|
||||
_fs_encoding,
|
||||
_NamedTemporaryFile,
|
||||
finalize,
|
||||
fs_decode,
|
||||
fs_encode,
|
||||
IS_TYPE_CHECKING,
|
||||
)
|
||||
|
||||
if IS_TYPE_CHECKING:
|
||||
@@ -343,8 +343,19 @@ def set_write_bit(fn):
|
||||
user_sid = get_current_user()
|
||||
icacls_exe = _find_icacls_exe() or "icacls"
|
||||
from .misc import run
|
||||
|
||||
if user_sid:
|
||||
_, err = run([icacls_exe, "/grant", "{0}:WD".format(user_sid), "''{0}''".format(fn), "/T", "/C", "/Q"])
|
||||
_, err = run(
|
||||
[
|
||||
icacls_exe,
|
||||
"/grant",
|
||||
"{0}:WD".format(user_sid),
|
||||
"''{0}''".format(fn),
|
||||
"/T",
|
||||
"/C",
|
||||
"/Q",
|
||||
]
|
||||
)
|
||||
if not err:
|
||||
return
|
||||
|
||||
@@ -390,7 +401,7 @@ def rmtree(directory, ignore_errors=False, onerror=None):
|
||||
raise
|
||||
|
||||
|
||||
def _wait_for_files(path):
|
||||
def _wait_for_files(path): # pragma: no cover
|
||||
"""
|
||||
Retry with backoff up to 1 second to delete files from a directory.
|
||||
|
||||
@@ -448,7 +459,12 @@ def handle_remove_readonly(func, path, exc):
|
||||
set_write_bit(path)
|
||||
try:
|
||||
func(path)
|
||||
except (OSError, IOError, FileNotFoundError, PermissionError) as e:
|
||||
except (
|
||||
OSError,
|
||||
IOError,
|
||||
FileNotFoundError,
|
||||
PermissionError,
|
||||
) as e: # pragma: no cover
|
||||
if e.errno in PERM_ERRORS:
|
||||
if e.errno == errno.ENOENT:
|
||||
return
|
||||
|
||||
Vendored
+7
-7
@@ -19,18 +19,18 @@ from .termcolors import COLOR_MAP, COLORS, DISABLE_COLORS, colored
|
||||
|
||||
try:
|
||||
import yaspin
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
yaspin = None
|
||||
Spinners = None
|
||||
SpinBase = None
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
import yaspin.spinners
|
||||
import yaspin.core
|
||||
|
||||
Spinners = yaspin.spinners.Spinners
|
||||
SpinBase = yaspin.core.Yaspin
|
||||
|
||||
if os.name == "nt":
|
||||
if os.name == "nt": # pragma: no cover
|
||||
|
||||
def handler(signum, frame, spinner):
|
||||
"""Signal handler, used to gracefully shut down the ``spinner`` instance
|
||||
@@ -44,7 +44,7 @@ if os.name == "nt":
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
|
||||
def handler(signum, frame, spinner):
|
||||
"""Signal handler, used to gracefully shut down the ``spinner`` instance
|
||||
@@ -92,7 +92,7 @@ class DummySpinner(object):
|
||||
self._close_output_buffer()
|
||||
return False
|
||||
|
||||
def __getattr__(self, k):
|
||||
def __getattr__(self, k): # pragma: no cover
|
||||
try:
|
||||
retval = super(DummySpinner, self).__getattribute__(k)
|
||||
except AttributeError:
|
||||
@@ -253,7 +253,7 @@ class VistirSpinner(SpinBase):
|
||||
target.write(CLEAR_LINE)
|
||||
self._show_cursor(target=target)
|
||||
|
||||
def write(self, text):
|
||||
def write(self, text): # pragma: no cover
|
||||
if not self.write_to_stdout:
|
||||
return self.write_err(text)
|
||||
stdout = self.stdout
|
||||
@@ -267,7 +267,7 @@ class VistirSpinner(SpinBase):
|
||||
stdout.write(text)
|
||||
self.out_buff.write(text)
|
||||
|
||||
def write_err(self, text):
|
||||
def write_err(self, text): # pragma: no cover
|
||||
"""Write error text in the terminal without breaking the spinner."""
|
||||
stderr = self.stderr
|
||||
if self.stderr.closed:
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "0.14.1"
|
||||
__version__ = "0.14.3"
|
||||
|
||||
Vendored
+31
-25
@@ -82,6 +82,7 @@ class Yaspin(object):
|
||||
self._hide_spin = None
|
||||
self._spin_thread = None
|
||||
self._last_frame = None
|
||||
self._stdout_lock = threading.Lock()
|
||||
|
||||
# Signals
|
||||
|
||||
@@ -253,43 +254,47 @@ class Yaspin(object):
|
||||
thr_is_alive = self._spin_thread and self._spin_thread.is_alive()
|
||||
|
||||
if thr_is_alive and not self._hide_spin.is_set():
|
||||
# set the hidden spinner flag
|
||||
self._hide_spin.set()
|
||||
with self._stdout_lock:
|
||||
# set the hidden spinner flag
|
||||
self._hide_spin.set()
|
||||
|
||||
# clear the current line
|
||||
sys.stdout.write("\r")
|
||||
self._clear_line()
|
||||
# clear the current line
|
||||
sys.stdout.write("\r")
|
||||
self._clear_line()
|
||||
|
||||
# flush the stdout buffer so the current line can be rewritten to
|
||||
sys.stdout.flush()
|
||||
# flush the stdout buffer so the current line
|
||||
# can be rewritten to
|
||||
sys.stdout.flush()
|
||||
|
||||
def show(self):
|
||||
"""Show the hidden spinner."""
|
||||
thr_is_alive = self._spin_thread and self._spin_thread.is_alive()
|
||||
|
||||
if thr_is_alive and self._hide_spin.is_set():
|
||||
# clear the hidden spinner flag
|
||||
self._hide_spin.clear()
|
||||
with self._stdout_lock:
|
||||
# clear the hidden spinner flag
|
||||
self._hide_spin.clear()
|
||||
|
||||
# clear the current line so the spinner is not appended to it
|
||||
sys.stdout.write("\r")
|
||||
self._clear_line()
|
||||
# clear the current line so the spinner is not appended to it
|
||||
sys.stdout.write("\r")
|
||||
self._clear_line()
|
||||
|
||||
def write(self, text):
|
||||
"""Write text in the terminal without breaking the spinner."""
|
||||
# similar to tqdm.write()
|
||||
# https://pypi.python.org/pypi/tqdm#writing-messages
|
||||
sys.stdout.write("\r")
|
||||
self._clear_line()
|
||||
with self._stdout_lock:
|
||||
sys.stdout.write("\r")
|
||||
self._clear_line()
|
||||
|
||||
_text = to_unicode(text)
|
||||
if PY2:
|
||||
_text = _text.encode(ENCODING)
|
||||
_text = to_unicode(text)
|
||||
if PY2:
|
||||
_text = _text.encode(ENCODING)
|
||||
|
||||
# Ensure output is bytes for Py2 and Unicode for Py3
|
||||
assert isinstance(_text, builtin_str)
|
||||
# Ensure output is bytes for Py2 and Unicode for Py3
|
||||
assert isinstance(_text, builtin_str)
|
||||
|
||||
sys.stdout.write("{0}\n".format(_text))
|
||||
sys.stdout.write("{0}\n".format(_text))
|
||||
|
||||
def ok(self, text="OK"):
|
||||
"""Set Ok (success) finalizer to a spinner."""
|
||||
@@ -312,7 +317,8 @@ class Yaspin(object):
|
||||
# Should be stopped here, otherwise prints after
|
||||
# self._freeze call will mess up the spinner
|
||||
self.stop()
|
||||
sys.stdout.write(self._last_frame)
|
||||
with self._stdout_lock:
|
||||
sys.stdout.write(self._last_frame)
|
||||
|
||||
def _spin(self):
|
||||
while not self._stop_spin.is_set():
|
||||
@@ -327,13 +333,13 @@ class Yaspin(object):
|
||||
out = self._compose_out(spin_phase)
|
||||
|
||||
# Write
|
||||
sys.stdout.write(out)
|
||||
self._clear_line()
|
||||
sys.stdout.flush()
|
||||
with self._stdout_lock:
|
||||
sys.stdout.write(out)
|
||||
self._clear_line()
|
||||
sys.stdout.flush()
|
||||
|
||||
# Wait
|
||||
time.sleep(self._interval)
|
||||
sys.stdout.write("\b")
|
||||
|
||||
def _compose_color_func(self):
|
||||
fn = functools.partial(
|
||||
|
||||
Reference in New Issue
Block a user