Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/pip/_vendor/distlib/__init__.py +33 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/compat.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/database.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/index.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/locators.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/manifest.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/markers.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/metadata.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/resources.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/scripts.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/util.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/version.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/wheel.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/markers.py +162 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/scripts.py +447 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/t32.exe +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/version.py +750 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/w32.exe +0 -0
- lib/python3.12/site-packages/pip/_vendor/distlib/wheel.py +1100 -0
- lib/python3.12/site-packages/pip/_vendor/rich/_export_format.py +76 -0
- lib/python3.12/site-packages/pip/_vendor/rich/table.py +1007 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__init__.py +102 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_collections.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_version.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connection.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connectionpool.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/exceptions.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/fields.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/filepost.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/poolmanager.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/request.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/response.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/_collections.py +355 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/_version.py +2 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/connection.py +572 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/connectionpool.py +1140 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__init__.py +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/_appengine_environ.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/appengine.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/ntlmpool.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/pyopenssl.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/securetransport.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/socks.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py +36 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/bindings.cpython-312.pyc +0 -0
lib/python3.12/site-packages/pip/_vendor/distlib/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
#
|
| 3 |
+
# Copyright (C) 2012-2023 Vinay Sajip.
|
| 4 |
+
# Licensed to the Python Software Foundation under a contributor agreement.
|
| 5 |
+
# See LICENSE.txt and CONTRIBUTORS.txt.
|
| 6 |
+
#
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
__version__ = '0.3.9'
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class DistlibException(Exception):
|
| 13 |
+
pass
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from logging import NullHandler
|
| 18 |
+
except ImportError: # pragma: no cover
|
| 19 |
+
|
| 20 |
+
class NullHandler(logging.Handler):
|
| 21 |
+
|
| 22 |
+
def handle(self, record):
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
def emit(self, record):
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
def createLock(self):
|
| 29 |
+
self.lock = None
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
logger.addHandler(NullHandler())
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.28 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/compat.cpython-312.pyc
ADDED
|
Binary file (45.5 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/database.cpython-312.pyc
ADDED
|
Binary file (65.6 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/index.cpython-312.pyc
ADDED
|
Binary file (24.3 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/locators.cpython-312.pyc
ADDED
|
Binary file (59.9 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/manifest.cpython-312.pyc
ADDED
|
Binary file (15.1 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/markers.cpython-312.pyc
ADDED
|
Binary file (7.66 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/metadata.cpython-312.pyc
ADDED
|
Binary file (41.6 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/resources.cpython-312.pyc
ADDED
|
Binary file (17.3 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/scripts.cpython-312.pyc
ADDED
|
Binary file (19.8 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/util.cpython-312.pyc
ADDED
|
Binary file (88 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/version.cpython-312.pyc
ADDED
|
Binary file (30.4 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/__pycache__/wheel.cpython-312.pyc
ADDED
|
Binary file (52.6 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/markers.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
#
|
| 3 |
+
# Copyright (C) 2012-2023 Vinay Sajip.
|
| 4 |
+
# Licensed to the Python Software Foundation under a contributor agreement.
|
| 5 |
+
# See LICENSE.txt and CONTRIBUTORS.txt.
|
| 6 |
+
#
|
| 7 |
+
"""
|
| 8 |
+
Parser for the environment markers micro-language defined in PEP 508.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
# Note: In PEP 345, the micro-language was Python compatible, so the ast
|
| 12 |
+
# module could be used to parse it. However, PEP 508 introduced operators such
|
| 13 |
+
# as ~= and === which aren't in Python, necessitating a different approach.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import re
|
| 17 |
+
import sys
|
| 18 |
+
import platform
|
| 19 |
+
|
| 20 |
+
from .compat import string_types
|
| 21 |
+
from .util import in_venv, parse_marker
|
| 22 |
+
from .version import LegacyVersion as LV
|
| 23 |
+
|
| 24 |
+
__all__ = ['interpret']
|
| 25 |
+
|
| 26 |
+
_VERSION_PATTERN = re.compile(r'((\d+(\.\d+)*\w*)|\'(\d+(\.\d+)*\w*)\'|\"(\d+(\.\d+)*\w*)\")')
|
| 27 |
+
_VERSION_MARKERS = {'python_version', 'python_full_version'}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _is_version_marker(s):
|
| 31 |
+
return isinstance(s, string_types) and s in _VERSION_MARKERS
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _is_literal(o):
|
| 35 |
+
if not isinstance(o, string_types) or not o:
|
| 36 |
+
return False
|
| 37 |
+
return o[0] in '\'"'
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _get_versions(s):
|
| 41 |
+
return {LV(m.groups()[0]) for m in _VERSION_PATTERN.finditer(s)}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class Evaluator(object):
|
| 45 |
+
"""
|
| 46 |
+
This class is used to evaluate marker expressions.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
operations = {
|
| 50 |
+
'==': lambda x, y: x == y,
|
| 51 |
+
'===': lambda x, y: x == y,
|
| 52 |
+
'~=': lambda x, y: x == y or x > y,
|
| 53 |
+
'!=': lambda x, y: x != y,
|
| 54 |
+
'<': lambda x, y: x < y,
|
| 55 |
+
'<=': lambda x, y: x == y or x < y,
|
| 56 |
+
'>': lambda x, y: x > y,
|
| 57 |
+
'>=': lambda x, y: x == y or x > y,
|
| 58 |
+
'and': lambda x, y: x and y,
|
| 59 |
+
'or': lambda x, y: x or y,
|
| 60 |
+
'in': lambda x, y: x in y,
|
| 61 |
+
'not in': lambda x, y: x not in y,
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
def evaluate(self, expr, context):
|
| 65 |
+
"""
|
| 66 |
+
Evaluate a marker expression returned by the :func:`parse_requirement`
|
| 67 |
+
function in the specified context.
|
| 68 |
+
"""
|
| 69 |
+
if isinstance(expr, string_types):
|
| 70 |
+
if expr[0] in '\'"':
|
| 71 |
+
result = expr[1:-1]
|
| 72 |
+
else:
|
| 73 |
+
if expr not in context:
|
| 74 |
+
raise SyntaxError('unknown variable: %s' % expr)
|
| 75 |
+
result = context[expr]
|
| 76 |
+
else:
|
| 77 |
+
assert isinstance(expr, dict)
|
| 78 |
+
op = expr['op']
|
| 79 |
+
if op not in self.operations:
|
| 80 |
+
raise NotImplementedError('op not implemented: %s' % op)
|
| 81 |
+
elhs = expr['lhs']
|
| 82 |
+
erhs = expr['rhs']
|
| 83 |
+
if _is_literal(expr['lhs']) and _is_literal(expr['rhs']):
|
| 84 |
+
raise SyntaxError('invalid comparison: %s %s %s' % (elhs, op, erhs))
|
| 85 |
+
|
| 86 |
+
lhs = self.evaluate(elhs, context)
|
| 87 |
+
rhs = self.evaluate(erhs, context)
|
| 88 |
+
if ((_is_version_marker(elhs) or _is_version_marker(erhs)) and
|
| 89 |
+
op in ('<', '<=', '>', '>=', '===', '==', '!=', '~=')):
|
| 90 |
+
lhs = LV(lhs)
|
| 91 |
+
rhs = LV(rhs)
|
| 92 |
+
elif _is_version_marker(elhs) and op in ('in', 'not in'):
|
| 93 |
+
lhs = LV(lhs)
|
| 94 |
+
rhs = _get_versions(rhs)
|
| 95 |
+
result = self.operations[op](lhs, rhs)
|
| 96 |
+
return result
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
_DIGITS = re.compile(r'\d+\.\d+')
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def default_context():
|
| 103 |
+
|
| 104 |
+
def format_full_version(info):
|
| 105 |
+
version = '%s.%s.%s' % (info.major, info.minor, info.micro)
|
| 106 |
+
kind = info.releaselevel
|
| 107 |
+
if kind != 'final':
|
| 108 |
+
version += kind[0] + str(info.serial)
|
| 109 |
+
return version
|
| 110 |
+
|
| 111 |
+
if hasattr(sys, 'implementation'):
|
| 112 |
+
implementation_version = format_full_version(sys.implementation.version)
|
| 113 |
+
implementation_name = sys.implementation.name
|
| 114 |
+
else:
|
| 115 |
+
implementation_version = '0'
|
| 116 |
+
implementation_name = ''
|
| 117 |
+
|
| 118 |
+
ppv = platform.python_version()
|
| 119 |
+
m = _DIGITS.match(ppv)
|
| 120 |
+
pv = m.group(0)
|
| 121 |
+
result = {
|
| 122 |
+
'implementation_name': implementation_name,
|
| 123 |
+
'implementation_version': implementation_version,
|
| 124 |
+
'os_name': os.name,
|
| 125 |
+
'platform_machine': platform.machine(),
|
| 126 |
+
'platform_python_implementation': platform.python_implementation(),
|
| 127 |
+
'platform_release': platform.release(),
|
| 128 |
+
'platform_system': platform.system(),
|
| 129 |
+
'platform_version': platform.version(),
|
| 130 |
+
'platform_in_venv': str(in_venv()),
|
| 131 |
+
'python_full_version': ppv,
|
| 132 |
+
'python_version': pv,
|
| 133 |
+
'sys_platform': sys.platform,
|
| 134 |
+
}
|
| 135 |
+
return result
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
DEFAULT_CONTEXT = default_context()
|
| 139 |
+
del default_context
|
| 140 |
+
|
| 141 |
+
evaluator = Evaluator()
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def interpret(marker, execution_context=None):
|
| 145 |
+
"""
|
| 146 |
+
Interpret a marker and return a result depending on environment.
|
| 147 |
+
|
| 148 |
+
:param marker: The marker to interpret.
|
| 149 |
+
:type marker: str
|
| 150 |
+
:param execution_context: The context used for name lookup.
|
| 151 |
+
:type execution_context: mapping
|
| 152 |
+
"""
|
| 153 |
+
try:
|
| 154 |
+
expr, rest = parse_marker(marker)
|
| 155 |
+
except Exception as e:
|
| 156 |
+
raise SyntaxError('Unable to interpret marker syntax: %s: %s' % (marker, e))
|
| 157 |
+
if rest and rest[0] != '#':
|
| 158 |
+
raise SyntaxError('unexpected trailing data in marker: %s: %s' % (marker, rest))
|
| 159 |
+
context = dict(DEFAULT_CONTEXT)
|
| 160 |
+
if execution_context:
|
| 161 |
+
context.update(execution_context)
|
| 162 |
+
return evaluator.evaluate(expr, context)
|
lib/python3.12/site-packages/pip/_vendor/distlib/scripts.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
#
|
| 3 |
+
# Copyright (C) 2013-2023 Vinay Sajip.
|
| 4 |
+
# Licensed to the Python Software Foundation under a contributor agreement.
|
| 5 |
+
# See LICENSE.txt and CONTRIBUTORS.txt.
|
| 6 |
+
#
|
| 7 |
+
from io import BytesIO
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import struct
|
| 12 |
+
import sys
|
| 13 |
+
import time
|
| 14 |
+
from zipfile import ZipInfo
|
| 15 |
+
|
| 16 |
+
from .compat import sysconfig, detect_encoding, ZipFile
|
| 17 |
+
from .resources import finder
|
| 18 |
+
from .util import (FileOperator, get_export_entry, convert_path, get_executable, get_platform, in_venv)
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
_DEFAULT_MANIFEST = '''
|
| 23 |
+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
| 24 |
+
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
| 25 |
+
<assemblyIdentity version="1.0.0.0"
|
| 26 |
+
processorArchitecture="X86"
|
| 27 |
+
name="%s"
|
| 28 |
+
type="win32"/>
|
| 29 |
+
|
| 30 |
+
<!-- Identify the application security requirements. -->
|
| 31 |
+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
| 32 |
+
<security>
|
| 33 |
+
<requestedPrivileges>
|
| 34 |
+
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
| 35 |
+
</requestedPrivileges>
|
| 36 |
+
</security>
|
| 37 |
+
</trustInfo>
|
| 38 |
+
</assembly>'''.strip()
|
| 39 |
+
|
| 40 |
+
# check if Python is called on the first line with this expression
|
| 41 |
+
FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
|
| 42 |
+
SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*-
|
| 43 |
+
import re
|
| 44 |
+
import sys
|
| 45 |
+
from %(module)s import %(import_name)s
|
| 46 |
+
if __name__ == '__main__':
|
| 47 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 48 |
+
sys.exit(%(func)s())
|
| 49 |
+
'''
|
| 50 |
+
|
| 51 |
+
# Pre-fetch the contents of all executable wrapper stubs.
|
| 52 |
+
# This is to address https://github.com/pypa/pip/issues/12666.
|
| 53 |
+
# When updating pip, we rename the old pip in place before installing the
|
| 54 |
+
# new version. If we try to fetch a wrapper *after* that rename, the finder
|
| 55 |
+
# machinery will be confused as the package is no longer available at the
|
| 56 |
+
# location where it was imported from. So we load everything into memory in
|
| 57 |
+
# advance.
|
| 58 |
+
|
| 59 |
+
if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'):
|
| 60 |
+
# Issue 31: don't hardcode an absolute package name, but
|
| 61 |
+
# determine it relative to the current package
|
| 62 |
+
DISTLIB_PACKAGE = __name__.rsplit('.', 1)[0]
|
| 63 |
+
|
| 64 |
+
WRAPPERS = {
|
| 65 |
+
r.name: r.bytes
|
| 66 |
+
for r in finder(DISTLIB_PACKAGE).iterator("")
|
| 67 |
+
if r.name.endswith(".exe")
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def enquote_executable(executable):
|
| 72 |
+
if ' ' in executable:
|
| 73 |
+
# make sure we quote only the executable in case of env
|
| 74 |
+
# for example /usr/bin/env "/dir with spaces/bin/jython"
|
| 75 |
+
# instead of "/usr/bin/env /dir with spaces/bin/jython"
|
| 76 |
+
# otherwise whole
|
| 77 |
+
if executable.startswith('/usr/bin/env '):
|
| 78 |
+
env, _executable = executable.split(' ', 1)
|
| 79 |
+
if ' ' in _executable and not _executable.startswith('"'):
|
| 80 |
+
executable = '%s "%s"' % (env, _executable)
|
| 81 |
+
else:
|
| 82 |
+
if not executable.startswith('"'):
|
| 83 |
+
executable = '"%s"' % executable
|
| 84 |
+
return executable
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# Keep the old name around (for now), as there is at least one project using it!
|
| 88 |
+
_enquote_executable = enquote_executable
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class ScriptMaker(object):
|
| 92 |
+
"""
|
| 93 |
+
A class to copy or create scripts from source scripts or callable
|
| 94 |
+
specifications.
|
| 95 |
+
"""
|
| 96 |
+
script_template = SCRIPT_TEMPLATE
|
| 97 |
+
|
| 98 |
+
executable = None # for shebangs
|
| 99 |
+
|
| 100 |
+
def __init__(self, source_dir, target_dir, add_launchers=True, dry_run=False, fileop=None):
|
| 101 |
+
self.source_dir = source_dir
|
| 102 |
+
self.target_dir = target_dir
|
| 103 |
+
self.add_launchers = add_launchers
|
| 104 |
+
self.force = False
|
| 105 |
+
self.clobber = False
|
| 106 |
+
# It only makes sense to set mode bits on POSIX.
|
| 107 |
+
self.set_mode = (os.name == 'posix') or (os.name == 'java' and os._name == 'posix')
|
| 108 |
+
self.variants = set(('', 'X.Y'))
|
| 109 |
+
self._fileop = fileop or FileOperator(dry_run)
|
| 110 |
+
|
| 111 |
+
self._is_nt = os.name == 'nt' or (os.name == 'java' and os._name == 'nt')
|
| 112 |
+
self.version_info = sys.version_info
|
| 113 |
+
|
| 114 |
+
def _get_alternate_executable(self, executable, options):
|
| 115 |
+
if options.get('gui', False) and self._is_nt: # pragma: no cover
|
| 116 |
+
dn, fn = os.path.split(executable)
|
| 117 |
+
fn = fn.replace('python', 'pythonw')
|
| 118 |
+
executable = os.path.join(dn, fn)
|
| 119 |
+
return executable
|
| 120 |
+
|
| 121 |
+
if sys.platform.startswith('java'): # pragma: no cover
|
| 122 |
+
|
| 123 |
+
def _is_shell(self, executable):
|
| 124 |
+
"""
|
| 125 |
+
Determine if the specified executable is a script
|
| 126 |
+
(contains a #! line)
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
with open(executable) as fp:
|
| 130 |
+
return fp.read(2) == '#!'
|
| 131 |
+
except (OSError, IOError):
|
| 132 |
+
logger.warning('Failed to open %s', executable)
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
def _fix_jython_executable(self, executable):
|
| 136 |
+
if self._is_shell(executable):
|
| 137 |
+
# Workaround for Jython is not needed on Linux systems.
|
| 138 |
+
import java
|
| 139 |
+
|
| 140 |
+
if java.lang.System.getProperty('os.name') == 'Linux':
|
| 141 |
+
return executable
|
| 142 |
+
elif executable.lower().endswith('jython.exe'):
|
| 143 |
+
# Use wrapper exe for Jython on Windows
|
| 144 |
+
return executable
|
| 145 |
+
return '/usr/bin/env %s' % executable
|
| 146 |
+
|
| 147 |
+
def _build_shebang(self, executable, post_interp):
|
| 148 |
+
"""
|
| 149 |
+
Build a shebang line. In the simple case (on Windows, or a shebang line
|
| 150 |
+
which is not too long or contains spaces) use a simple formulation for
|
| 151 |
+
the shebang. Otherwise, use /bin/sh as the executable, with a contrived
|
| 152 |
+
shebang which allows the script to run either under Python or sh, using
|
| 153 |
+
suitable quoting. Thanks to Harald Nordgren for his input.
|
| 154 |
+
|
| 155 |
+
See also: http://www.in-ulm.de/~mascheck/various/shebang/#length
|
| 156 |
+
https://hg.mozilla.org/mozilla-central/file/tip/mach
|
| 157 |
+
"""
|
| 158 |
+
if os.name != 'posix':
|
| 159 |
+
simple_shebang = True
|
| 160 |
+
elif getattr(sys, "cross_compiling", False):
|
| 161 |
+
# In a cross-compiling environment, the shebang will likely be a
|
| 162 |
+
# script; this *must* be invoked with the "safe" version of the
|
| 163 |
+
# shebang, or else using os.exec() to run the entry script will
|
| 164 |
+
# fail, raising "OSError 8 [Errno 8] Exec format error".
|
| 165 |
+
simple_shebang = False
|
| 166 |
+
else:
|
| 167 |
+
# Add 3 for '#!' prefix and newline suffix.
|
| 168 |
+
shebang_length = len(executable) + len(post_interp) + 3
|
| 169 |
+
if sys.platform == 'darwin':
|
| 170 |
+
max_shebang_length = 512
|
| 171 |
+
else:
|
| 172 |
+
max_shebang_length = 127
|
| 173 |
+
simple_shebang = ((b' ' not in executable) and (shebang_length <= max_shebang_length))
|
| 174 |
+
|
| 175 |
+
if simple_shebang:
|
| 176 |
+
result = b'#!' + executable + post_interp + b'\n'
|
| 177 |
+
else:
|
| 178 |
+
result = b'#!/bin/sh\n'
|
| 179 |
+
result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n'
|
| 180 |
+
result += b"' '''\n"
|
| 181 |
+
return result
|
| 182 |
+
|
| 183 |
+
def _get_shebang(self, encoding, post_interp=b'', options=None):
|
| 184 |
+
enquote = True
|
| 185 |
+
if self.executable:
|
| 186 |
+
executable = self.executable
|
| 187 |
+
enquote = False # assume this will be taken care of
|
| 188 |
+
elif not sysconfig.is_python_build():
|
| 189 |
+
executable = get_executable()
|
| 190 |
+
elif in_venv(): # pragma: no cover
|
| 191 |
+
executable = os.path.join(sysconfig.get_path('scripts'), 'python%s' % sysconfig.get_config_var('EXE'))
|
| 192 |
+
else: # pragma: no cover
|
| 193 |
+
if os.name == 'nt':
|
| 194 |
+
# for Python builds from source on Windows, no Python executables with
|
| 195 |
+
# a version suffix are created, so we use python.exe
|
| 196 |
+
executable = os.path.join(sysconfig.get_config_var('BINDIR'),
|
| 197 |
+
'python%s' % (sysconfig.get_config_var('EXE')))
|
| 198 |
+
else:
|
| 199 |
+
executable = os.path.join(
|
| 200 |
+
sysconfig.get_config_var('BINDIR'),
|
| 201 |
+
'python%s%s' % (sysconfig.get_config_var('VERSION'), sysconfig.get_config_var('EXE')))
|
| 202 |
+
if options:
|
| 203 |
+
executable = self._get_alternate_executable(executable, options)
|
| 204 |
+
|
| 205 |
+
if sys.platform.startswith('java'): # pragma: no cover
|
| 206 |
+
executable = self._fix_jython_executable(executable)
|
| 207 |
+
|
| 208 |
+
# Normalise case for Windows - COMMENTED OUT
|
| 209 |
+
# executable = os.path.normcase(executable)
|
| 210 |
+
# N.B. The normalising operation above has been commented out: See
|
| 211 |
+
# issue #124. Although paths in Windows are generally case-insensitive,
|
| 212 |
+
# they aren't always. For example, a path containing a ẞ (which is a
|
| 213 |
+
# LATIN CAPITAL LETTER SHARP S - U+1E9E) is normcased to ß (which is a
|
| 214 |
+
# LATIN SMALL LETTER SHARP S' - U+00DF). The two are not considered by
|
| 215 |
+
# Windows as equivalent in path names.
|
| 216 |
+
|
| 217 |
+
# If the user didn't specify an executable, it may be necessary to
|
| 218 |
+
# cater for executable paths with spaces (not uncommon on Windows)
|
| 219 |
+
if enquote:
|
| 220 |
+
executable = enquote_executable(executable)
|
| 221 |
+
# Issue #51: don't use fsencode, since we later try to
|
| 222 |
+
# check that the shebang is decodable using utf-8.
|
| 223 |
+
executable = executable.encode('utf-8')
|
| 224 |
+
# in case of IronPython, play safe and enable frames support
|
| 225 |
+
if (sys.platform == 'cli' and '-X:Frames' not in post_interp and
|
| 226 |
+
'-X:FullFrames' not in post_interp): # pragma: no cover
|
| 227 |
+
post_interp += b' -X:Frames'
|
| 228 |
+
shebang = self._build_shebang(executable, post_interp)
|
| 229 |
+
# Python parser starts to read a script using UTF-8 until
|
| 230 |
+
# it gets a #coding:xxx cookie. The shebang has to be the
|
| 231 |
+
# first line of a file, the #coding:xxx cookie cannot be
|
| 232 |
+
# written before. So the shebang has to be decodable from
|
| 233 |
+
# UTF-8.
|
| 234 |
+
try:
|
| 235 |
+
shebang.decode('utf-8')
|
| 236 |
+
except UnicodeDecodeError: # pragma: no cover
|
| 237 |
+
raise ValueError('The shebang (%r) is not decodable from utf-8' % shebang)
|
| 238 |
+
# If the script is encoded to a custom encoding (use a
|
| 239 |
+
# #coding:xxx cookie), the shebang has to be decodable from
|
| 240 |
+
# the script encoding too.
|
| 241 |
+
if encoding != 'utf-8':
|
| 242 |
+
try:
|
| 243 |
+
shebang.decode(encoding)
|
| 244 |
+
except UnicodeDecodeError: # pragma: no cover
|
| 245 |
+
raise ValueError('The shebang (%r) is not decodable '
|
| 246 |
+
'from the script encoding (%r)' % (shebang, encoding))
|
| 247 |
+
return shebang
|
| 248 |
+
|
| 249 |
+
def _get_script_text(self, entry):
|
| 250 |
+
return self.script_template % dict(
|
| 251 |
+
module=entry.prefix, import_name=entry.suffix.split('.')[0], func=entry.suffix)
|
| 252 |
+
|
| 253 |
+
manifest = _DEFAULT_MANIFEST
|
| 254 |
+
|
| 255 |
+
def get_manifest(self, exename):
|
| 256 |
+
base = os.path.basename(exename)
|
| 257 |
+
return self.manifest % base
|
| 258 |
+
|
| 259 |
+
def _write_script(self, names, shebang, script_bytes, filenames, ext):
|
| 260 |
+
use_launcher = self.add_launchers and self._is_nt
|
| 261 |
+
if not use_launcher:
|
| 262 |
+
script_bytes = shebang + script_bytes
|
| 263 |
+
else: # pragma: no cover
|
| 264 |
+
if ext == 'py':
|
| 265 |
+
launcher = self._get_launcher('t')
|
| 266 |
+
else:
|
| 267 |
+
launcher = self._get_launcher('w')
|
| 268 |
+
stream = BytesIO()
|
| 269 |
+
with ZipFile(stream, 'w') as zf:
|
| 270 |
+
source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
|
| 271 |
+
if source_date_epoch:
|
| 272 |
+
date_time = time.gmtime(int(source_date_epoch))[:6]
|
| 273 |
+
zinfo = ZipInfo(filename='__main__.py', date_time=date_time)
|
| 274 |
+
zf.writestr(zinfo, script_bytes)
|
| 275 |
+
else:
|
| 276 |
+
zf.writestr('__main__.py', script_bytes)
|
| 277 |
+
zip_data = stream.getvalue()
|
| 278 |
+
script_bytes = launcher + shebang + zip_data
|
| 279 |
+
for name in names:
|
| 280 |
+
outname = os.path.join(self.target_dir, name)
|
| 281 |
+
if use_launcher: # pragma: no cover
|
| 282 |
+
n, e = os.path.splitext(outname)
|
| 283 |
+
if e.startswith('.py'):
|
| 284 |
+
outname = n
|
| 285 |
+
outname = '%s.exe' % outname
|
| 286 |
+
try:
|
| 287 |
+
self._fileop.write_binary_file(outname, script_bytes)
|
| 288 |
+
except Exception:
|
| 289 |
+
# Failed writing an executable - it might be in use.
|
| 290 |
+
logger.warning('Failed to write executable - trying to '
|
| 291 |
+
'use .deleteme logic')
|
| 292 |
+
dfname = '%s.deleteme' % outname
|
| 293 |
+
if os.path.exists(dfname):
|
| 294 |
+
os.remove(dfname) # Not allowed to fail here
|
| 295 |
+
os.rename(outname, dfname) # nor here
|
| 296 |
+
self._fileop.write_binary_file(outname, script_bytes)
|
| 297 |
+
logger.debug('Able to replace executable using '
|
| 298 |
+
'.deleteme logic')
|
| 299 |
+
try:
|
| 300 |
+
os.remove(dfname)
|
| 301 |
+
except Exception:
|
| 302 |
+
pass # still in use - ignore error
|
| 303 |
+
else:
|
| 304 |
+
if self._is_nt and not outname.endswith('.' + ext): # pragma: no cover
|
| 305 |
+
outname = '%s.%s' % (outname, ext)
|
| 306 |
+
if os.path.exists(outname) and not self.clobber:
|
| 307 |
+
logger.warning('Skipping existing file %s', outname)
|
| 308 |
+
continue
|
| 309 |
+
self._fileop.write_binary_file(outname, script_bytes)
|
| 310 |
+
if self.set_mode:
|
| 311 |
+
self._fileop.set_executable_mode([outname])
|
| 312 |
+
filenames.append(outname)
|
| 313 |
+
|
| 314 |
+
variant_separator = '-'
|
| 315 |
+
|
| 316 |
+
def get_script_filenames(self, name):
|
| 317 |
+
result = set()
|
| 318 |
+
if '' in self.variants:
|
| 319 |
+
result.add(name)
|
| 320 |
+
if 'X' in self.variants:
|
| 321 |
+
result.add('%s%s' % (name, self.version_info[0]))
|
| 322 |
+
if 'X.Y' in self.variants:
|
| 323 |
+
result.add('%s%s%s.%s' % (name, self.variant_separator, self.version_info[0], self.version_info[1]))
|
| 324 |
+
return result
|
| 325 |
+
|
| 326 |
+
def _make_script(self, entry, filenames, options=None):
|
| 327 |
+
post_interp = b''
|
| 328 |
+
if options:
|
| 329 |
+
args = options.get('interpreter_args', [])
|
| 330 |
+
if args:
|
| 331 |
+
args = ' %s' % ' '.join(args)
|
| 332 |
+
post_interp = args.encode('utf-8')
|
| 333 |
+
shebang = self._get_shebang('utf-8', post_interp, options=options)
|
| 334 |
+
script = self._get_script_text(entry).encode('utf-8')
|
| 335 |
+
scriptnames = self.get_script_filenames(entry.name)
|
| 336 |
+
if options and options.get('gui', False):
|
| 337 |
+
ext = 'pyw'
|
| 338 |
+
else:
|
| 339 |
+
ext = 'py'
|
| 340 |
+
self._write_script(scriptnames, shebang, script, filenames, ext)
|
| 341 |
+
|
| 342 |
+
def _copy_script(self, script, filenames):
|
| 343 |
+
adjust = False
|
| 344 |
+
script = os.path.join(self.source_dir, convert_path(script))
|
| 345 |
+
outname = os.path.join(self.target_dir, os.path.basename(script))
|
| 346 |
+
if not self.force and not self._fileop.newer(script, outname):
|
| 347 |
+
logger.debug('not copying %s (up-to-date)', script)
|
| 348 |
+
return
|
| 349 |
+
|
| 350 |
+
# Always open the file, but ignore failures in dry-run mode --
|
| 351 |
+
# that way, we'll get accurate feedback if we can read the
|
| 352 |
+
# script.
|
| 353 |
+
try:
|
| 354 |
+
f = open(script, 'rb')
|
| 355 |
+
except IOError: # pragma: no cover
|
| 356 |
+
if not self.dry_run:
|
| 357 |
+
raise
|
| 358 |
+
f = None
|
| 359 |
+
else:
|
| 360 |
+
first_line = f.readline()
|
| 361 |
+
if not first_line: # pragma: no cover
|
| 362 |
+
logger.warning('%s is an empty file (skipping)', script)
|
| 363 |
+
return
|
| 364 |
+
|
| 365 |
+
match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n'))
|
| 366 |
+
if match:
|
| 367 |
+
adjust = True
|
| 368 |
+
post_interp = match.group(1) or b''
|
| 369 |
+
|
| 370 |
+
if not adjust:
|
| 371 |
+
if f:
|
| 372 |
+
f.close()
|
| 373 |
+
self._fileop.copy_file(script, outname)
|
| 374 |
+
if self.set_mode:
|
| 375 |
+
self._fileop.set_executable_mode([outname])
|
| 376 |
+
filenames.append(outname)
|
| 377 |
+
else:
|
| 378 |
+
logger.info('copying and adjusting %s -> %s', script, self.target_dir)
|
| 379 |
+
if not self._fileop.dry_run:
|
| 380 |
+
encoding, lines = detect_encoding(f.readline)
|
| 381 |
+
f.seek(0)
|
| 382 |
+
shebang = self._get_shebang(encoding, post_interp)
|
| 383 |
+
if b'pythonw' in first_line: # pragma: no cover
|
| 384 |
+
ext = 'pyw'
|
| 385 |
+
else:
|
| 386 |
+
ext = 'py'
|
| 387 |
+
n = os.path.basename(outname)
|
| 388 |
+
self._write_script([n], shebang, f.read(), filenames, ext)
|
| 389 |
+
if f:
|
| 390 |
+
f.close()
|
| 391 |
+
|
| 392 |
+
@property
|
| 393 |
+
def dry_run(self):
|
| 394 |
+
return self._fileop.dry_run
|
| 395 |
+
|
| 396 |
+
@dry_run.setter
|
| 397 |
+
def dry_run(self, value):
|
| 398 |
+
self._fileop.dry_run = value
|
| 399 |
+
|
| 400 |
+
if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'): # pragma: no cover
|
| 401 |
+
# Executable launcher support.
|
| 402 |
+
# Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
|
| 403 |
+
|
| 404 |
+
def _get_launcher(self, kind):
|
| 405 |
+
if struct.calcsize('P') == 8: # 64-bit
|
| 406 |
+
bits = '64'
|
| 407 |
+
else:
|
| 408 |
+
bits = '32'
|
| 409 |
+
platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
|
| 410 |
+
name = '%s%s%s.exe' % (kind, bits, platform_suffix)
|
| 411 |
+
if name not in WRAPPERS:
|
| 412 |
+
msg = ('Unable to find resource %s in package %s' %
|
| 413 |
+
(name, DISTLIB_PACKAGE))
|
| 414 |
+
raise ValueError(msg)
|
| 415 |
+
return WRAPPERS[name]
|
| 416 |
+
|
| 417 |
+
# Public API follows
|
| 418 |
+
|
| 419 |
+
def make(self, specification, options=None):
|
| 420 |
+
"""
|
| 421 |
+
Make a script.
|
| 422 |
+
|
| 423 |
+
:param specification: The specification, which is either a valid export
|
| 424 |
+
entry specification (to make a script from a
|
| 425 |
+
callable) or a filename (to make a script by
|
| 426 |
+
copying from a source location).
|
| 427 |
+
:param options: A dictionary of options controlling script generation.
|
| 428 |
+
:return: A list of all absolute pathnames written to.
|
| 429 |
+
"""
|
| 430 |
+
filenames = []
|
| 431 |
+
entry = get_export_entry(specification)
|
| 432 |
+
if entry is None:
|
| 433 |
+
self._copy_script(specification, filenames)
|
| 434 |
+
else:
|
| 435 |
+
self._make_script(entry, filenames, options=options)
|
| 436 |
+
return filenames
|
| 437 |
+
|
| 438 |
+
def make_multiple(self, specifications, options=None):
|
| 439 |
+
"""
|
| 440 |
+
Take a list of specifications and make scripts from them,
|
| 441 |
+
:param specifications: A list of specifications.
|
| 442 |
+
:return: A list of all absolute pathnames written to,
|
| 443 |
+
"""
|
| 444 |
+
filenames = []
|
| 445 |
+
for specification in specifications:
|
| 446 |
+
filenames.extend(self.make(specification, options))
|
| 447 |
+
return filenames
|
lib/python3.12/site-packages/pip/_vendor/distlib/t32.exe
ADDED
|
Binary file (97.8 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/version.py
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
#
|
| 3 |
+
# Copyright (C) 2012-2023 The Python Software Foundation.
|
| 4 |
+
# See LICENSE.txt and CONTRIBUTORS.txt.
|
| 5 |
+
#
|
| 6 |
+
"""
|
| 7 |
+
Implementation of a flexible versioning scheme providing support for PEP-440,
|
| 8 |
+
setuptools-compatible and semantic versioning.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import logging
|
| 12 |
+
import re
|
| 13 |
+
|
| 14 |
+
from .compat import string_types
|
| 15 |
+
from .util import parse_requirement
|
| 16 |
+
|
| 17 |
+
__all__ = ['NormalizedVersion', 'NormalizedMatcher',
|
| 18 |
+
'LegacyVersion', 'LegacyMatcher',
|
| 19 |
+
'SemanticVersion', 'SemanticMatcher',
|
| 20 |
+
'UnsupportedVersionError', 'get_scheme']
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class UnsupportedVersionError(ValueError):
|
| 26 |
+
"""This is an unsupported version."""
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Version(object):
|
| 31 |
+
def __init__(self, s):
|
| 32 |
+
self._string = s = s.strip()
|
| 33 |
+
self._parts = parts = self.parse(s)
|
| 34 |
+
assert isinstance(parts, tuple)
|
| 35 |
+
assert len(parts) > 0
|
| 36 |
+
|
| 37 |
+
def parse(self, s):
|
| 38 |
+
raise NotImplementedError('please implement in a subclass')
|
| 39 |
+
|
| 40 |
+
def _check_compatible(self, other):
|
| 41 |
+
if type(self) != type(other):
|
| 42 |
+
raise TypeError('cannot compare %r and %r' % (self, other))
|
| 43 |
+
|
| 44 |
+
def __eq__(self, other):
|
| 45 |
+
self._check_compatible(other)
|
| 46 |
+
return self._parts == other._parts
|
| 47 |
+
|
| 48 |
+
def __ne__(self, other):
|
| 49 |
+
return not self.__eq__(other)
|
| 50 |
+
|
| 51 |
+
def __lt__(self, other):
|
| 52 |
+
self._check_compatible(other)
|
| 53 |
+
return self._parts < other._parts
|
| 54 |
+
|
| 55 |
+
def __gt__(self, other):
|
| 56 |
+
return not (self.__lt__(other) or self.__eq__(other))
|
| 57 |
+
|
| 58 |
+
def __le__(self, other):
|
| 59 |
+
return self.__lt__(other) or self.__eq__(other)
|
| 60 |
+
|
| 61 |
+
def __ge__(self, other):
|
| 62 |
+
return self.__gt__(other) or self.__eq__(other)
|
| 63 |
+
|
| 64 |
+
# See http://docs.python.org/reference/datamodel#object.__hash__
|
| 65 |
+
def __hash__(self):
|
| 66 |
+
return hash(self._parts)
|
| 67 |
+
|
| 68 |
+
def __repr__(self):
|
| 69 |
+
return "%s('%s')" % (self.__class__.__name__, self._string)
|
| 70 |
+
|
| 71 |
+
def __str__(self):
|
| 72 |
+
return self._string
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def is_prerelease(self):
|
| 76 |
+
raise NotImplementedError('Please implement in subclasses.')
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class Matcher(object):
|
| 80 |
+
version_class = None
|
| 81 |
+
|
| 82 |
+
# value is either a callable or the name of a method
|
| 83 |
+
_operators = {
|
| 84 |
+
'<': lambda v, c, p: v < c,
|
| 85 |
+
'>': lambda v, c, p: v > c,
|
| 86 |
+
'<=': lambda v, c, p: v == c or v < c,
|
| 87 |
+
'>=': lambda v, c, p: v == c or v > c,
|
| 88 |
+
'==': lambda v, c, p: v == c,
|
| 89 |
+
'===': lambda v, c, p: v == c,
|
| 90 |
+
# by default, compatible => >=.
|
| 91 |
+
'~=': lambda v, c, p: v == c or v > c,
|
| 92 |
+
'!=': lambda v, c, p: v != c,
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# this is a method only to support alternative implementations
|
| 96 |
+
# via overriding
|
| 97 |
+
def parse_requirement(self, s):
|
| 98 |
+
return parse_requirement(s)
|
| 99 |
+
|
| 100 |
+
def __init__(self, s):
|
| 101 |
+
if self.version_class is None:
|
| 102 |
+
raise ValueError('Please specify a version class')
|
| 103 |
+
self._string = s = s.strip()
|
| 104 |
+
r = self.parse_requirement(s)
|
| 105 |
+
if not r:
|
| 106 |
+
raise ValueError('Not valid: %r' % s)
|
| 107 |
+
self.name = r.name
|
| 108 |
+
self.key = self.name.lower() # for case-insensitive comparisons
|
| 109 |
+
clist = []
|
| 110 |
+
if r.constraints:
|
| 111 |
+
# import pdb; pdb.set_trace()
|
| 112 |
+
for op, s in r.constraints:
|
| 113 |
+
if s.endswith('.*'):
|
| 114 |
+
if op not in ('==', '!='):
|
| 115 |
+
raise ValueError('\'.*\' not allowed for '
|
| 116 |
+
'%r constraints' % op)
|
| 117 |
+
# Could be a partial version (e.g. for '2.*') which
|
| 118 |
+
# won't parse as a version, so keep it as a string
|
| 119 |
+
vn, prefix = s[:-2], True
|
| 120 |
+
# Just to check that vn is a valid version
|
| 121 |
+
self.version_class(vn)
|
| 122 |
+
else:
|
| 123 |
+
# Should parse as a version, so we can create an
|
| 124 |
+
# instance for the comparison
|
| 125 |
+
vn, prefix = self.version_class(s), False
|
| 126 |
+
clist.append((op, vn, prefix))
|
| 127 |
+
self._parts = tuple(clist)
|
| 128 |
+
|
| 129 |
+
def match(self, version):
|
| 130 |
+
"""
|
| 131 |
+
Check if the provided version matches the constraints.
|
| 132 |
+
|
| 133 |
+
:param version: The version to match against this instance.
|
| 134 |
+
:type version: String or :class:`Version` instance.
|
| 135 |
+
"""
|
| 136 |
+
if isinstance(version, string_types):
|
| 137 |
+
version = self.version_class(version)
|
| 138 |
+
for operator, constraint, prefix in self._parts:
|
| 139 |
+
f = self._operators.get(operator)
|
| 140 |
+
if isinstance(f, string_types):
|
| 141 |
+
f = getattr(self, f)
|
| 142 |
+
if not f:
|
| 143 |
+
msg = ('%r not implemented '
|
| 144 |
+
'for %s' % (operator, self.__class__.__name__))
|
| 145 |
+
raise NotImplementedError(msg)
|
| 146 |
+
if not f(version, constraint, prefix):
|
| 147 |
+
return False
|
| 148 |
+
return True
|
| 149 |
+
|
| 150 |
+
@property
|
| 151 |
+
def exact_version(self):
|
| 152 |
+
result = None
|
| 153 |
+
if len(self._parts) == 1 and self._parts[0][0] in ('==', '==='):
|
| 154 |
+
result = self._parts[0][1]
|
| 155 |
+
return result
|
| 156 |
+
|
| 157 |
+
def _check_compatible(self, other):
|
| 158 |
+
if type(self) != type(other) or self.name != other.name:
|
| 159 |
+
raise TypeError('cannot compare %s and %s' % (self, other))
|
| 160 |
+
|
| 161 |
+
def __eq__(self, other):
|
| 162 |
+
self._check_compatible(other)
|
| 163 |
+
return self.key == other.key and self._parts == other._parts
|
| 164 |
+
|
| 165 |
+
def __ne__(self, other):
|
| 166 |
+
return not self.__eq__(other)
|
| 167 |
+
|
| 168 |
+
# See http://docs.python.org/reference/datamodel#object.__hash__
|
| 169 |
+
def __hash__(self):
|
| 170 |
+
return hash(self.key) + hash(self._parts)
|
| 171 |
+
|
| 172 |
+
def __repr__(self):
|
| 173 |
+
return "%s(%r)" % (self.__class__.__name__, self._string)
|
| 174 |
+
|
| 175 |
+
def __str__(self):
|
| 176 |
+
return self._string
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
PEP440_VERSION_RE = re.compile(r'^v?(\d+!)?(\d+(\.\d+)*)((a|alpha|b|beta|c|rc|pre|preview)(\d+)?)?'
|
| 180 |
+
r'(\.(post|r|rev)(\d+)?)?([._-]?(dev)(\d+)?)?'
|
| 181 |
+
r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$', re.I)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _pep_440_key(s):
|
| 185 |
+
s = s.strip()
|
| 186 |
+
m = PEP440_VERSION_RE.match(s)
|
| 187 |
+
if not m:
|
| 188 |
+
raise UnsupportedVersionError('Not a valid version: %s' % s)
|
| 189 |
+
groups = m.groups()
|
| 190 |
+
nums = tuple(int(v) for v in groups[1].split('.'))
|
| 191 |
+
while len(nums) > 1 and nums[-1] == 0:
|
| 192 |
+
nums = nums[:-1]
|
| 193 |
+
|
| 194 |
+
if not groups[0]:
|
| 195 |
+
epoch = 0
|
| 196 |
+
else:
|
| 197 |
+
epoch = int(groups[0][:-1])
|
| 198 |
+
pre = groups[4:6]
|
| 199 |
+
post = groups[7:9]
|
| 200 |
+
dev = groups[10:12]
|
| 201 |
+
local = groups[13]
|
| 202 |
+
if pre == (None, None):
|
| 203 |
+
pre = ()
|
| 204 |
+
else:
|
| 205 |
+
if pre[1] is None:
|
| 206 |
+
pre = pre[0], 0
|
| 207 |
+
else:
|
| 208 |
+
pre = pre[0], int(pre[1])
|
| 209 |
+
if post == (None, None):
|
| 210 |
+
post = ()
|
| 211 |
+
else:
|
| 212 |
+
if post[1] is None:
|
| 213 |
+
post = post[0], 0
|
| 214 |
+
else:
|
| 215 |
+
post = post[0], int(post[1])
|
| 216 |
+
if dev == (None, None):
|
| 217 |
+
dev = ()
|
| 218 |
+
else:
|
| 219 |
+
if dev[1] is None:
|
| 220 |
+
dev = dev[0], 0
|
| 221 |
+
else:
|
| 222 |
+
dev = dev[0], int(dev[1])
|
| 223 |
+
if local is None:
|
| 224 |
+
local = ()
|
| 225 |
+
else:
|
| 226 |
+
parts = []
|
| 227 |
+
for part in local.split('.'):
|
| 228 |
+
# to ensure that numeric compares as > lexicographic, avoid
|
| 229 |
+
# comparing them directly, but encode a tuple which ensures
|
| 230 |
+
# correct sorting
|
| 231 |
+
if part.isdigit():
|
| 232 |
+
part = (1, int(part))
|
| 233 |
+
else:
|
| 234 |
+
part = (0, part)
|
| 235 |
+
parts.append(part)
|
| 236 |
+
local = tuple(parts)
|
| 237 |
+
if not pre:
|
| 238 |
+
# either before pre-release, or final release and after
|
| 239 |
+
if not post and dev:
|
| 240 |
+
# before pre-release
|
| 241 |
+
pre = ('a', -1) # to sort before a0
|
| 242 |
+
else:
|
| 243 |
+
pre = ('z',) # to sort after all pre-releases
|
| 244 |
+
# now look at the state of post and dev.
|
| 245 |
+
if not post:
|
| 246 |
+
post = ('_',) # sort before 'a'
|
| 247 |
+
if not dev:
|
| 248 |
+
dev = ('final',)
|
| 249 |
+
|
| 250 |
+
return epoch, nums, pre, post, dev, local
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
_normalized_key = _pep_440_key
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class NormalizedVersion(Version):
|
| 257 |
+
"""A rational version.
|
| 258 |
+
|
| 259 |
+
Good:
|
| 260 |
+
1.2 # equivalent to "1.2.0"
|
| 261 |
+
1.2.0
|
| 262 |
+
1.2a1
|
| 263 |
+
1.2.3a2
|
| 264 |
+
1.2.3b1
|
| 265 |
+
1.2.3c1
|
| 266 |
+
1.2.3.4
|
| 267 |
+
TODO: fill this out
|
| 268 |
+
|
| 269 |
+
Bad:
|
| 270 |
+
1 # minimum two numbers
|
| 271 |
+
1.2a # release level must have a release serial
|
| 272 |
+
1.2.3b
|
| 273 |
+
"""
|
| 274 |
+
def parse(self, s):
|
| 275 |
+
result = _normalized_key(s)
|
| 276 |
+
# _normalized_key loses trailing zeroes in the release
|
| 277 |
+
# clause, since that's needed to ensure that X.Y == X.Y.0 == X.Y.0.0
|
| 278 |
+
# However, PEP 440 prefix matching needs it: for example,
|
| 279 |
+
# (~= 1.4.5.0) matches differently to (~= 1.4.5.0.0).
|
| 280 |
+
m = PEP440_VERSION_RE.match(s) # must succeed
|
| 281 |
+
groups = m.groups()
|
| 282 |
+
self._release_clause = tuple(int(v) for v in groups[1].split('.'))
|
| 283 |
+
return result
|
| 284 |
+
|
| 285 |
+
PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
|
| 286 |
+
|
| 287 |
+
@property
|
| 288 |
+
def is_prerelease(self):
|
| 289 |
+
return any(t[0] in self.PREREL_TAGS for t in self._parts if t)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def _match_prefix(x, y):
|
| 293 |
+
x = str(x)
|
| 294 |
+
y = str(y)
|
| 295 |
+
if x == y:
|
| 296 |
+
return True
|
| 297 |
+
if not x.startswith(y):
|
| 298 |
+
return False
|
| 299 |
+
n = len(y)
|
| 300 |
+
return x[n] == '.'
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
class NormalizedMatcher(Matcher):
|
| 304 |
+
version_class = NormalizedVersion
|
| 305 |
+
|
| 306 |
+
# value is either a callable or the name of a method
|
| 307 |
+
_operators = {
|
| 308 |
+
'~=': '_match_compatible',
|
| 309 |
+
'<': '_match_lt',
|
| 310 |
+
'>': '_match_gt',
|
| 311 |
+
'<=': '_match_le',
|
| 312 |
+
'>=': '_match_ge',
|
| 313 |
+
'==': '_match_eq',
|
| 314 |
+
'===': '_match_arbitrary',
|
| 315 |
+
'!=': '_match_ne',
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
def _adjust_local(self, version, constraint, prefix):
|
| 319 |
+
if prefix:
|
| 320 |
+
strip_local = '+' not in constraint and version._parts[-1]
|
| 321 |
+
else:
|
| 322 |
+
# both constraint and version are
|
| 323 |
+
# NormalizedVersion instances.
|
| 324 |
+
# If constraint does not have a local component,
|
| 325 |
+
# ensure the version doesn't, either.
|
| 326 |
+
strip_local = not constraint._parts[-1] and version._parts[-1]
|
| 327 |
+
if strip_local:
|
| 328 |
+
s = version._string.split('+', 1)[0]
|
| 329 |
+
version = self.version_class(s)
|
| 330 |
+
return version, constraint
|
| 331 |
+
|
| 332 |
+
def _match_lt(self, version, constraint, prefix):
|
| 333 |
+
version, constraint = self._adjust_local(version, constraint, prefix)
|
| 334 |
+
if version >= constraint:
|
| 335 |
+
return False
|
| 336 |
+
release_clause = constraint._release_clause
|
| 337 |
+
pfx = '.'.join([str(i) for i in release_clause])
|
| 338 |
+
return not _match_prefix(version, pfx)
|
| 339 |
+
|
| 340 |
+
def _match_gt(self, version, constraint, prefix):
|
| 341 |
+
version, constraint = self._adjust_local(version, constraint, prefix)
|
| 342 |
+
if version <= constraint:
|
| 343 |
+
return False
|
| 344 |
+
release_clause = constraint._release_clause
|
| 345 |
+
pfx = '.'.join([str(i) for i in release_clause])
|
| 346 |
+
return not _match_prefix(version, pfx)
|
| 347 |
+
|
| 348 |
+
def _match_le(self, version, constraint, prefix):
|
| 349 |
+
version, constraint = self._adjust_local(version, constraint, prefix)
|
| 350 |
+
return version <= constraint
|
| 351 |
+
|
| 352 |
+
def _match_ge(self, version, constraint, prefix):
|
| 353 |
+
version, constraint = self._adjust_local(version, constraint, prefix)
|
| 354 |
+
return version >= constraint
|
| 355 |
+
|
| 356 |
+
def _match_eq(self, version, constraint, prefix):
|
| 357 |
+
version, constraint = self._adjust_local(version, constraint, prefix)
|
| 358 |
+
if not prefix:
|
| 359 |
+
result = (version == constraint)
|
| 360 |
+
else:
|
| 361 |
+
result = _match_prefix(version, constraint)
|
| 362 |
+
return result
|
| 363 |
+
|
| 364 |
+
def _match_arbitrary(self, version, constraint, prefix):
|
| 365 |
+
return str(version) == str(constraint)
|
| 366 |
+
|
| 367 |
+
def _match_ne(self, version, constraint, prefix):
|
| 368 |
+
version, constraint = self._adjust_local(version, constraint, prefix)
|
| 369 |
+
if not prefix:
|
| 370 |
+
result = (version != constraint)
|
| 371 |
+
else:
|
| 372 |
+
result = not _match_prefix(version, constraint)
|
| 373 |
+
return result
|
| 374 |
+
|
| 375 |
+
def _match_compatible(self, version, constraint, prefix):
|
| 376 |
+
version, constraint = self._adjust_local(version, constraint, prefix)
|
| 377 |
+
if version == constraint:
|
| 378 |
+
return True
|
| 379 |
+
if version < constraint:
|
| 380 |
+
return False
|
| 381 |
+
# if not prefix:
|
| 382 |
+
# return True
|
| 383 |
+
release_clause = constraint._release_clause
|
| 384 |
+
if len(release_clause) > 1:
|
| 385 |
+
release_clause = release_clause[:-1]
|
| 386 |
+
pfx = '.'.join([str(i) for i in release_clause])
|
| 387 |
+
return _match_prefix(version, pfx)
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
_REPLACEMENTS = (
|
| 391 |
+
(re.compile('[.+-]$'), ''), # remove trailing puncts
|
| 392 |
+
(re.compile(r'^[.](\d)'), r'0.\1'), # .N -> 0.N at start
|
| 393 |
+
(re.compile('^[.-]'), ''), # remove leading puncts
|
| 394 |
+
(re.compile(r'^\((.*)\)$'), r'\1'), # remove parentheses
|
| 395 |
+
(re.compile(r'^v(ersion)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
|
| 396 |
+
(re.compile(r'^r(ev)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
|
| 397 |
+
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
|
| 398 |
+
(re.compile(r'\b(alfa|apha)\b'), 'alpha'), # misspelt alpha
|
| 399 |
+
(re.compile(r'\b(pre-alpha|prealpha)\b'),
|
| 400 |
+
'pre.alpha'), # standardise
|
| 401 |
+
(re.compile(r'\(beta\)$'), 'beta'), # remove parentheses
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
_SUFFIX_REPLACEMENTS = (
|
| 405 |
+
(re.compile('^[:~._+-]+'), ''), # remove leading puncts
|
| 406 |
+
(re.compile('[,*")([\\]]'), ''), # remove unwanted chars
|
| 407 |
+
(re.compile('[~:+_ -]'), '.'), # replace illegal chars
|
| 408 |
+
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
|
| 409 |
+
(re.compile(r'\.$'), ''), # trailing '.'
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
_NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)')
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def _suggest_semantic_version(s):
|
| 416 |
+
"""
|
| 417 |
+
Try to suggest a semantic form for a version for which
|
| 418 |
+
_suggest_normalized_version couldn't come up with anything.
|
| 419 |
+
"""
|
| 420 |
+
result = s.strip().lower()
|
| 421 |
+
for pat, repl in _REPLACEMENTS:
|
| 422 |
+
result = pat.sub(repl, result)
|
| 423 |
+
if not result:
|
| 424 |
+
result = '0.0.0'
|
| 425 |
+
|
| 426 |
+
# Now look for numeric prefix, and separate it out from
|
| 427 |
+
# the rest.
|
| 428 |
+
# import pdb; pdb.set_trace()
|
| 429 |
+
m = _NUMERIC_PREFIX.match(result)
|
| 430 |
+
if not m:
|
| 431 |
+
prefix = '0.0.0'
|
| 432 |
+
suffix = result
|
| 433 |
+
else:
|
| 434 |
+
prefix = m.groups()[0].split('.')
|
| 435 |
+
prefix = [int(i) for i in prefix]
|
| 436 |
+
while len(prefix) < 3:
|
| 437 |
+
prefix.append(0)
|
| 438 |
+
if len(prefix) == 3:
|
| 439 |
+
suffix = result[m.end():]
|
| 440 |
+
else:
|
| 441 |
+
suffix = '.'.join([str(i) for i in prefix[3:]]) + result[m.end():]
|
| 442 |
+
prefix = prefix[:3]
|
| 443 |
+
prefix = '.'.join([str(i) for i in prefix])
|
| 444 |
+
suffix = suffix.strip()
|
| 445 |
+
if suffix:
|
| 446 |
+
# import pdb; pdb.set_trace()
|
| 447 |
+
# massage the suffix.
|
| 448 |
+
for pat, repl in _SUFFIX_REPLACEMENTS:
|
| 449 |
+
suffix = pat.sub(repl, suffix)
|
| 450 |
+
|
| 451 |
+
if not suffix:
|
| 452 |
+
result = prefix
|
| 453 |
+
else:
|
| 454 |
+
sep = '-' if 'dev' in suffix else '+'
|
| 455 |
+
result = prefix + sep + suffix
|
| 456 |
+
if not is_semver(result):
|
| 457 |
+
result = None
|
| 458 |
+
return result
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _suggest_normalized_version(s):
|
| 462 |
+
"""Suggest a normalized version close to the given version string.
|
| 463 |
+
|
| 464 |
+
If you have a version string that isn't rational (i.e. NormalizedVersion
|
| 465 |
+
doesn't like it) then you might be able to get an equivalent (or close)
|
| 466 |
+
rational version from this function.
|
| 467 |
+
|
| 468 |
+
This does a number of simple normalizations to the given string, based
|
| 469 |
+
on observation of versions currently in use on PyPI. Given a dump of
|
| 470 |
+
those version during PyCon 2009, 4287 of them:
|
| 471 |
+
- 2312 (53.93%) match NormalizedVersion without change
|
| 472 |
+
with the automatic suggestion
|
| 473 |
+
- 3474 (81.04%) match when using this suggestion method
|
| 474 |
+
|
| 475 |
+
@param s {str} An irrational version string.
|
| 476 |
+
@returns A rational version string, or None, if couldn't determine one.
|
| 477 |
+
"""
|
| 478 |
+
try:
|
| 479 |
+
_normalized_key(s)
|
| 480 |
+
return s # already rational
|
| 481 |
+
except UnsupportedVersionError:
|
| 482 |
+
pass
|
| 483 |
+
|
| 484 |
+
rs = s.lower()
|
| 485 |
+
|
| 486 |
+
# part of this could use maketrans
|
| 487 |
+
for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
|
| 488 |
+
('beta', 'b'), ('rc', 'c'), ('-final', ''),
|
| 489 |
+
('-pre', 'c'),
|
| 490 |
+
('-release', ''), ('.release', ''), ('-stable', ''),
|
| 491 |
+
('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
|
| 492 |
+
('final', '')):
|
| 493 |
+
rs = rs.replace(orig, repl)
|
| 494 |
+
|
| 495 |
+
# if something ends with dev or pre, we add a 0
|
| 496 |
+
rs = re.sub(r"pre$", r"pre0", rs)
|
| 497 |
+
rs = re.sub(r"dev$", r"dev0", rs)
|
| 498 |
+
|
| 499 |
+
# if we have something like "b-2" or "a.2" at the end of the
|
| 500 |
+
# version, that is probably beta, alpha, etc
|
| 501 |
+
# let's remove the dash or dot
|
| 502 |
+
rs = re.sub(r"([abc]|rc)[\-\.](\d+)$", r"\1\2", rs)
|
| 503 |
+
|
| 504 |
+
# 1.0-dev-r371 -> 1.0.dev371
|
| 505 |
+
# 0.1-dev-r79 -> 0.1.dev79
|
| 506 |
+
rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
|
| 507 |
+
|
| 508 |
+
# Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
|
| 509 |
+
rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
|
| 510 |
+
|
| 511 |
+
# Clean: v0.3, v1.0
|
| 512 |
+
if rs.startswith('v'):
|
| 513 |
+
rs = rs[1:]
|
| 514 |
+
|
| 515 |
+
# Clean leading '0's on numbers.
|
| 516 |
+
# TODO: unintended side-effect on, e.g., "2003.05.09"
|
| 517 |
+
# PyPI stats: 77 (~2%) better
|
| 518 |
+
rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
|
| 519 |
+
|
| 520 |
+
# Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
|
| 521 |
+
# zero.
|
| 522 |
+
# PyPI stats: 245 (7.56%) better
|
| 523 |
+
rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
|
| 524 |
+
|
| 525 |
+
# the 'dev-rNNN' tag is a dev tag
|
| 526 |
+
rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
|
| 527 |
+
|
| 528 |
+
# clean the - when used as a pre delimiter
|
| 529 |
+
rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
|
| 530 |
+
|
| 531 |
+
# a terminal "dev" or "devel" can be changed into ".dev0"
|
| 532 |
+
rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
|
| 533 |
+
|
| 534 |
+
# a terminal "dev" can be changed into ".dev0"
|
| 535 |
+
rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
|
| 536 |
+
|
| 537 |
+
# a terminal "final" or "stable" can be removed
|
| 538 |
+
rs = re.sub(r"(final|stable)$", "", rs)
|
| 539 |
+
|
| 540 |
+
# The 'r' and the '-' tags are post release tags
|
| 541 |
+
# 0.4a1.r10 -> 0.4a1.post10
|
| 542 |
+
# 0.9.33-17222 -> 0.9.33.post17222
|
| 543 |
+
# 0.9.33-r17222 -> 0.9.33.post17222
|
| 544 |
+
rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
|
| 545 |
+
|
| 546 |
+
# Clean 'r' instead of 'dev' usage:
|
| 547 |
+
# 0.9.33+r17222 -> 0.9.33.dev17222
|
| 548 |
+
# 1.0dev123 -> 1.0.dev123
|
| 549 |
+
# 1.0.git123 -> 1.0.dev123
|
| 550 |
+
# 1.0.bzr123 -> 1.0.dev123
|
| 551 |
+
# 0.1a0dev.123 -> 0.1a0.dev123
|
| 552 |
+
# PyPI stats: ~150 (~4%) better
|
| 553 |
+
rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
|
| 554 |
+
|
| 555 |
+
# Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
|
| 556 |
+
# 0.2.pre1 -> 0.2c1
|
| 557 |
+
# 0.2-c1 -> 0.2c1
|
| 558 |
+
# 1.0preview123 -> 1.0c123
|
| 559 |
+
# PyPI stats: ~21 (0.62%) better
|
| 560 |
+
rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
|
| 561 |
+
|
| 562 |
+
# Tcl/Tk uses "px" for their post release markers
|
| 563 |
+
rs = re.sub(r"p(\d+)$", r".post\1", rs)
|
| 564 |
+
|
| 565 |
+
try:
|
| 566 |
+
_normalized_key(rs)
|
| 567 |
+
except UnsupportedVersionError:
|
| 568 |
+
rs = None
|
| 569 |
+
return rs
|
| 570 |
+
|
| 571 |
+
#
|
| 572 |
+
# Legacy version processing (distribute-compatible)
|
| 573 |
+
#
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
_VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
|
| 577 |
+
_VERSION_REPLACE = {
|
| 578 |
+
'pre': 'c',
|
| 579 |
+
'preview': 'c',
|
| 580 |
+
'-': 'final-',
|
| 581 |
+
'rc': 'c',
|
| 582 |
+
'dev': '@',
|
| 583 |
+
'': None,
|
| 584 |
+
'.': None,
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def _legacy_key(s):
|
| 589 |
+
def get_parts(s):
|
| 590 |
+
result = []
|
| 591 |
+
for p in _VERSION_PART.split(s.lower()):
|
| 592 |
+
p = _VERSION_REPLACE.get(p, p)
|
| 593 |
+
if p:
|
| 594 |
+
if '0' <= p[:1] <= '9':
|
| 595 |
+
p = p.zfill(8)
|
| 596 |
+
else:
|
| 597 |
+
p = '*' + p
|
| 598 |
+
result.append(p)
|
| 599 |
+
result.append('*final')
|
| 600 |
+
return result
|
| 601 |
+
|
| 602 |
+
result = []
|
| 603 |
+
for p in get_parts(s):
|
| 604 |
+
if p.startswith('*'):
|
| 605 |
+
if p < '*final':
|
| 606 |
+
while result and result[-1] == '*final-':
|
| 607 |
+
result.pop()
|
| 608 |
+
while result and result[-1] == '00000000':
|
| 609 |
+
result.pop()
|
| 610 |
+
result.append(p)
|
| 611 |
+
return tuple(result)
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
class LegacyVersion(Version):
|
| 615 |
+
def parse(self, s):
|
| 616 |
+
return _legacy_key(s)
|
| 617 |
+
|
| 618 |
+
@property
|
| 619 |
+
def is_prerelease(self):
|
| 620 |
+
result = False
|
| 621 |
+
for x in self._parts:
|
| 622 |
+
if (isinstance(x, string_types) and x.startswith('*') and x < '*final'):
|
| 623 |
+
result = True
|
| 624 |
+
break
|
| 625 |
+
return result
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
class LegacyMatcher(Matcher):
|
| 629 |
+
version_class = LegacyVersion
|
| 630 |
+
|
| 631 |
+
_operators = dict(Matcher._operators)
|
| 632 |
+
_operators['~='] = '_match_compatible'
|
| 633 |
+
|
| 634 |
+
numeric_re = re.compile(r'^(\d+(\.\d+)*)')
|
| 635 |
+
|
| 636 |
+
def _match_compatible(self, version, constraint, prefix):
|
| 637 |
+
if version < constraint:
|
| 638 |
+
return False
|
| 639 |
+
m = self.numeric_re.match(str(constraint))
|
| 640 |
+
if not m:
|
| 641 |
+
logger.warning('Cannot compute compatible match for version %s '
|
| 642 |
+
' and constraint %s', version, constraint)
|
| 643 |
+
return True
|
| 644 |
+
s = m.groups()[0]
|
| 645 |
+
if '.' in s:
|
| 646 |
+
s = s.rsplit('.', 1)[0]
|
| 647 |
+
return _match_prefix(version, s)
|
| 648 |
+
|
| 649 |
+
#
|
| 650 |
+
# Semantic versioning
|
| 651 |
+
#
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
_SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
|
| 655 |
+
r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
|
| 656 |
+
r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def is_semver(s):
|
| 660 |
+
return _SEMVER_RE.match(s)
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def _semantic_key(s):
|
| 664 |
+
def make_tuple(s, absent):
|
| 665 |
+
if s is None:
|
| 666 |
+
result = (absent,)
|
| 667 |
+
else:
|
| 668 |
+
parts = s[1:].split('.')
|
| 669 |
+
# We can't compare ints and strings on Python 3, so fudge it
|
| 670 |
+
# by zero-filling numeric values so simulate a numeric comparison
|
| 671 |
+
result = tuple([p.zfill(8) if p.isdigit() else p for p in parts])
|
| 672 |
+
return result
|
| 673 |
+
|
| 674 |
+
m = is_semver(s)
|
| 675 |
+
if not m:
|
| 676 |
+
raise UnsupportedVersionError(s)
|
| 677 |
+
groups = m.groups()
|
| 678 |
+
major, minor, patch = [int(i) for i in groups[:3]]
|
| 679 |
+
# choose the '|' and '*' so that versions sort correctly
|
| 680 |
+
pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*')
|
| 681 |
+
return (major, minor, patch), pre, build
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
class SemanticVersion(Version):
|
| 685 |
+
def parse(self, s):
|
| 686 |
+
return _semantic_key(s)
|
| 687 |
+
|
| 688 |
+
@property
|
| 689 |
+
def is_prerelease(self):
|
| 690 |
+
return self._parts[1][0] != '|'
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
class SemanticMatcher(Matcher):
|
| 694 |
+
version_class = SemanticVersion
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
class VersionScheme(object):
|
| 698 |
+
def __init__(self, key, matcher, suggester=None):
|
| 699 |
+
self.key = key
|
| 700 |
+
self.matcher = matcher
|
| 701 |
+
self.suggester = suggester
|
| 702 |
+
|
| 703 |
+
def is_valid_version(self, s):
|
| 704 |
+
try:
|
| 705 |
+
self.matcher.version_class(s)
|
| 706 |
+
result = True
|
| 707 |
+
except UnsupportedVersionError:
|
| 708 |
+
result = False
|
| 709 |
+
return result
|
| 710 |
+
|
| 711 |
+
def is_valid_matcher(self, s):
|
| 712 |
+
try:
|
| 713 |
+
self.matcher(s)
|
| 714 |
+
result = True
|
| 715 |
+
except UnsupportedVersionError:
|
| 716 |
+
result = False
|
| 717 |
+
return result
|
| 718 |
+
|
| 719 |
+
def is_valid_constraint_list(self, s):
|
| 720 |
+
"""
|
| 721 |
+
Used for processing some metadata fields
|
| 722 |
+
"""
|
| 723 |
+
# See issue #140. Be tolerant of a single trailing comma.
|
| 724 |
+
if s.endswith(','):
|
| 725 |
+
s = s[:-1]
|
| 726 |
+
return self.is_valid_matcher('dummy_name (%s)' % s)
|
| 727 |
+
|
| 728 |
+
def suggest(self, s):
|
| 729 |
+
if self.suggester is None:
|
| 730 |
+
result = None
|
| 731 |
+
else:
|
| 732 |
+
result = self.suggester(s)
|
| 733 |
+
return result
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
_SCHEMES = {
|
| 737 |
+
'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
|
| 738 |
+
_suggest_normalized_version),
|
| 739 |
+
'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
|
| 740 |
+
'semantic': VersionScheme(_semantic_key, SemanticMatcher,
|
| 741 |
+
_suggest_semantic_version),
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
_SCHEMES['default'] = _SCHEMES['normalized']
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
def get_scheme(name):
|
| 748 |
+
if name not in _SCHEMES:
|
| 749 |
+
raise ValueError('unknown scheme name: %r' % name)
|
| 750 |
+
return _SCHEMES[name]
|
lib/python3.12/site-packages/pip/_vendor/distlib/w32.exe
ADDED
|
Binary file (91.6 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/distlib/wheel.py
ADDED
|
@@ -0,0 +1,1100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
#
|
| 3 |
+
# Copyright (C) 2013-2023 Vinay Sajip.
|
| 4 |
+
# Licensed to the Python Software Foundation under a contributor agreement.
|
| 5 |
+
# See LICENSE.txt and CONTRIBUTORS.txt.
|
| 6 |
+
#
|
| 7 |
+
from __future__ import unicode_literals
|
| 8 |
+
|
| 9 |
+
import base64
|
| 10 |
+
import codecs
|
| 11 |
+
import datetime
|
| 12 |
+
from email import message_from_file
|
| 13 |
+
import hashlib
|
| 14 |
+
import json
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
import posixpath
|
| 18 |
+
import re
|
| 19 |
+
import shutil
|
| 20 |
+
import sys
|
| 21 |
+
import tempfile
|
| 22 |
+
import zipfile
|
| 23 |
+
|
| 24 |
+
from . import __version__, DistlibException
|
| 25 |
+
from .compat import sysconfig, ZipFile, fsdecode, text_type, filter
|
| 26 |
+
from .database import InstalledDistribution
|
| 27 |
+
from .metadata import Metadata, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME
|
| 28 |
+
from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache, cached_property, get_cache_base,
|
| 29 |
+
read_exports, tempdir, get_platform)
|
| 30 |
+
from .version import NormalizedVersion, UnsupportedVersionError
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
cache = None # created when needed
|
| 35 |
+
|
| 36 |
+
if hasattr(sys, 'pypy_version_info'): # pragma: no cover
|
| 37 |
+
IMP_PREFIX = 'pp'
|
| 38 |
+
elif sys.platform.startswith('java'): # pragma: no cover
|
| 39 |
+
IMP_PREFIX = 'jy'
|
| 40 |
+
elif sys.platform == 'cli': # pragma: no cover
|
| 41 |
+
IMP_PREFIX = 'ip'
|
| 42 |
+
else:
|
| 43 |
+
IMP_PREFIX = 'cp'
|
| 44 |
+
|
| 45 |
+
VER_SUFFIX = sysconfig.get_config_var('py_version_nodot')
|
| 46 |
+
if not VER_SUFFIX: # pragma: no cover
|
| 47 |
+
VER_SUFFIX = '%s%s' % sys.version_info[:2]
|
| 48 |
+
PYVER = 'py' + VER_SUFFIX
|
| 49 |
+
IMPVER = IMP_PREFIX + VER_SUFFIX
|
| 50 |
+
|
| 51 |
+
ARCH = get_platform().replace('-', '_').replace('.', '_')
|
| 52 |
+
|
| 53 |
+
ABI = sysconfig.get_config_var('SOABI')
|
| 54 |
+
if ABI and ABI.startswith('cpython-'):
|
| 55 |
+
ABI = ABI.replace('cpython-', 'cp').split('-')[0]
|
| 56 |
+
else:
|
| 57 |
+
|
| 58 |
+
def _derive_abi():
|
| 59 |
+
parts = ['cp', VER_SUFFIX]
|
| 60 |
+
if sysconfig.get_config_var('Py_DEBUG'):
|
| 61 |
+
parts.append('d')
|
| 62 |
+
if IMP_PREFIX == 'cp':
|
| 63 |
+
vi = sys.version_info[:2]
|
| 64 |
+
if vi < (3, 8):
|
| 65 |
+
wpm = sysconfig.get_config_var('WITH_PYMALLOC')
|
| 66 |
+
if wpm is None:
|
| 67 |
+
wpm = True
|
| 68 |
+
if wpm:
|
| 69 |
+
parts.append('m')
|
| 70 |
+
if vi < (3, 3):
|
| 71 |
+
us = sysconfig.get_config_var('Py_UNICODE_SIZE')
|
| 72 |
+
if us == 4 or (us is None and sys.maxunicode == 0x10FFFF):
|
| 73 |
+
parts.append('u')
|
| 74 |
+
return ''.join(parts)
|
| 75 |
+
|
| 76 |
+
ABI = _derive_abi()
|
| 77 |
+
del _derive_abi
|
| 78 |
+
|
| 79 |
+
FILENAME_RE = re.compile(
|
| 80 |
+
r'''
|
| 81 |
+
(?P<nm>[^-]+)
|
| 82 |
+
-(?P<vn>\d+[^-]*)
|
| 83 |
+
(-(?P<bn>\d+[^-]*))?
|
| 84 |
+
-(?P<py>\w+\d+(\.\w+\d+)*)
|
| 85 |
+
-(?P<bi>\w+)
|
| 86 |
+
-(?P<ar>\w+(\.\w+)*)
|
| 87 |
+
\.whl$
|
| 88 |
+
''', re.IGNORECASE | re.VERBOSE)
|
| 89 |
+
|
| 90 |
+
NAME_VERSION_RE = re.compile(r'''
|
| 91 |
+
(?P<nm>[^-]+)
|
| 92 |
+
-(?P<vn>\d+[^-]*)
|
| 93 |
+
(-(?P<bn>\d+[^-]*))?$
|
| 94 |
+
''', re.IGNORECASE | re.VERBOSE)
|
| 95 |
+
|
| 96 |
+
SHEBANG_RE = re.compile(br'\s*#![^\r\n]*')
|
| 97 |
+
SHEBANG_DETAIL_RE = re.compile(br'^(\s*#!("[^"]+"|\S+))\s+(.*)$')
|
| 98 |
+
SHEBANG_PYTHON = b'#!python'
|
| 99 |
+
SHEBANG_PYTHONW = b'#!pythonw'
|
| 100 |
+
|
| 101 |
+
if os.sep == '/':
|
| 102 |
+
to_posix = lambda o: o
|
| 103 |
+
else:
|
| 104 |
+
to_posix = lambda o: o.replace(os.sep, '/')
|
| 105 |
+
|
| 106 |
+
if sys.version_info[0] < 3:
|
| 107 |
+
import imp
|
| 108 |
+
else:
|
| 109 |
+
imp = None
|
| 110 |
+
import importlib.machinery
|
| 111 |
+
import importlib.util
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _get_suffixes():
|
| 115 |
+
if imp:
|
| 116 |
+
return [s[0] for s in imp.get_suffixes()]
|
| 117 |
+
else:
|
| 118 |
+
return importlib.machinery.EXTENSION_SUFFIXES
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _load_dynamic(name, path):
|
| 122 |
+
# https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
| 123 |
+
if imp:
|
| 124 |
+
return imp.load_dynamic(name, path)
|
| 125 |
+
else:
|
| 126 |
+
spec = importlib.util.spec_from_file_location(name, path)
|
| 127 |
+
module = importlib.util.module_from_spec(spec)
|
| 128 |
+
sys.modules[name] = module
|
| 129 |
+
spec.loader.exec_module(module)
|
| 130 |
+
return module
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class Mounter(object):
|
| 134 |
+
|
| 135 |
+
def __init__(self):
|
| 136 |
+
self.impure_wheels = {}
|
| 137 |
+
self.libs = {}
|
| 138 |
+
|
| 139 |
+
def add(self, pathname, extensions):
|
| 140 |
+
self.impure_wheels[pathname] = extensions
|
| 141 |
+
self.libs.update(extensions)
|
| 142 |
+
|
| 143 |
+
def remove(self, pathname):
|
| 144 |
+
extensions = self.impure_wheels.pop(pathname)
|
| 145 |
+
for k, v in extensions:
|
| 146 |
+
if k in self.libs:
|
| 147 |
+
del self.libs[k]
|
| 148 |
+
|
| 149 |
+
def find_module(self, fullname, path=None):
|
| 150 |
+
if fullname in self.libs:
|
| 151 |
+
result = self
|
| 152 |
+
else:
|
| 153 |
+
result = None
|
| 154 |
+
return result
|
| 155 |
+
|
| 156 |
+
def load_module(self, fullname):
|
| 157 |
+
if fullname in sys.modules:
|
| 158 |
+
result = sys.modules[fullname]
|
| 159 |
+
else:
|
| 160 |
+
if fullname not in self.libs:
|
| 161 |
+
raise ImportError('unable to find extension for %s' % fullname)
|
| 162 |
+
result = _load_dynamic(fullname, self.libs[fullname])
|
| 163 |
+
result.__loader__ = self
|
| 164 |
+
parts = fullname.rsplit('.', 1)
|
| 165 |
+
if len(parts) > 1:
|
| 166 |
+
result.__package__ = parts[0]
|
| 167 |
+
return result
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
_hook = Mounter()
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class Wheel(object):
|
| 174 |
+
"""
|
| 175 |
+
Class to build and install from Wheel files (PEP 427).
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
wheel_version = (1, 1)
|
| 179 |
+
hash_kind = 'sha256'
|
| 180 |
+
|
| 181 |
+
def __init__(self, filename=None, sign=False, verify=False):
|
| 182 |
+
"""
|
| 183 |
+
Initialise an instance using a (valid) filename.
|
| 184 |
+
"""
|
| 185 |
+
self.sign = sign
|
| 186 |
+
self.should_verify = verify
|
| 187 |
+
self.buildver = ''
|
| 188 |
+
self.pyver = [PYVER]
|
| 189 |
+
self.abi = ['none']
|
| 190 |
+
self.arch = ['any']
|
| 191 |
+
self.dirname = os.getcwd()
|
| 192 |
+
if filename is None:
|
| 193 |
+
self.name = 'dummy'
|
| 194 |
+
self.version = '0.1'
|
| 195 |
+
self._filename = self.filename
|
| 196 |
+
else:
|
| 197 |
+
m = NAME_VERSION_RE.match(filename)
|
| 198 |
+
if m:
|
| 199 |
+
info = m.groupdict('')
|
| 200 |
+
self.name = info['nm']
|
| 201 |
+
# Reinstate the local version separator
|
| 202 |
+
self.version = info['vn'].replace('_', '-')
|
| 203 |
+
self.buildver = info['bn']
|
| 204 |
+
self._filename = self.filename
|
| 205 |
+
else:
|
| 206 |
+
dirname, filename = os.path.split(filename)
|
| 207 |
+
m = FILENAME_RE.match(filename)
|
| 208 |
+
if not m:
|
| 209 |
+
raise DistlibException('Invalid name or '
|
| 210 |
+
'filename: %r' % filename)
|
| 211 |
+
if dirname:
|
| 212 |
+
self.dirname = os.path.abspath(dirname)
|
| 213 |
+
self._filename = filename
|
| 214 |
+
info = m.groupdict('')
|
| 215 |
+
self.name = info['nm']
|
| 216 |
+
self.version = info['vn']
|
| 217 |
+
self.buildver = info['bn']
|
| 218 |
+
self.pyver = info['py'].split('.')
|
| 219 |
+
self.abi = info['bi'].split('.')
|
| 220 |
+
self.arch = info['ar'].split('.')
|
| 221 |
+
|
| 222 |
+
@property
|
| 223 |
+
def filename(self):
|
| 224 |
+
"""
|
| 225 |
+
Build and return a filename from the various components.
|
| 226 |
+
"""
|
| 227 |
+
if self.buildver:
|
| 228 |
+
buildver = '-' + self.buildver
|
| 229 |
+
else:
|
| 230 |
+
buildver = ''
|
| 231 |
+
pyver = '.'.join(self.pyver)
|
| 232 |
+
abi = '.'.join(self.abi)
|
| 233 |
+
arch = '.'.join(self.arch)
|
| 234 |
+
# replace - with _ as a local version separator
|
| 235 |
+
version = self.version.replace('-', '_')
|
| 236 |
+
return '%s-%s%s-%s-%s-%s.whl' % (self.name, version, buildver, pyver, abi, arch)
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def exists(self):
|
| 240 |
+
path = os.path.join(self.dirname, self.filename)
|
| 241 |
+
return os.path.isfile(path)
|
| 242 |
+
|
| 243 |
+
@property
|
| 244 |
+
def tags(self):
|
| 245 |
+
for pyver in self.pyver:
|
| 246 |
+
for abi in self.abi:
|
| 247 |
+
for arch in self.arch:
|
| 248 |
+
yield pyver, abi, arch
|
| 249 |
+
|
| 250 |
+
@cached_property
|
| 251 |
+
def metadata(self):
|
| 252 |
+
pathname = os.path.join(self.dirname, self.filename)
|
| 253 |
+
name_ver = '%s-%s' % (self.name, self.version)
|
| 254 |
+
info_dir = '%s.dist-info' % name_ver
|
| 255 |
+
wrapper = codecs.getreader('utf-8')
|
| 256 |
+
with ZipFile(pathname, 'r') as zf:
|
| 257 |
+
self.get_wheel_metadata(zf)
|
| 258 |
+
# wv = wheel_metadata['Wheel-Version'].split('.', 1)
|
| 259 |
+
# file_version = tuple([int(i) for i in wv])
|
| 260 |
+
# if file_version < (1, 1):
|
| 261 |
+
# fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME,
|
| 262 |
+
# LEGACY_METADATA_FILENAME]
|
| 263 |
+
# else:
|
| 264 |
+
# fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME]
|
| 265 |
+
fns = [WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME]
|
| 266 |
+
result = None
|
| 267 |
+
for fn in fns:
|
| 268 |
+
try:
|
| 269 |
+
metadata_filename = posixpath.join(info_dir, fn)
|
| 270 |
+
with zf.open(metadata_filename) as bf:
|
| 271 |
+
wf = wrapper(bf)
|
| 272 |
+
result = Metadata(fileobj=wf)
|
| 273 |
+
if result:
|
| 274 |
+
break
|
| 275 |
+
except KeyError:
|
| 276 |
+
pass
|
| 277 |
+
if not result:
|
| 278 |
+
raise ValueError('Invalid wheel, because metadata is '
|
| 279 |
+
'missing: looked in %s' % ', '.join(fns))
|
| 280 |
+
return result
|
| 281 |
+
|
| 282 |
+
def get_wheel_metadata(self, zf):
|
| 283 |
+
name_ver = '%s-%s' % (self.name, self.version)
|
| 284 |
+
info_dir = '%s.dist-info' % name_ver
|
| 285 |
+
metadata_filename = posixpath.join(info_dir, 'WHEEL')
|
| 286 |
+
with zf.open(metadata_filename) as bf:
|
| 287 |
+
wf = codecs.getreader('utf-8')(bf)
|
| 288 |
+
message = message_from_file(wf)
|
| 289 |
+
return dict(message)
|
| 290 |
+
|
| 291 |
+
@cached_property
|
| 292 |
+
def info(self):
|
| 293 |
+
pathname = os.path.join(self.dirname, self.filename)
|
| 294 |
+
with ZipFile(pathname, 'r') as zf:
|
| 295 |
+
result = self.get_wheel_metadata(zf)
|
| 296 |
+
return result
|
| 297 |
+
|
| 298 |
+
def process_shebang(self, data):
|
| 299 |
+
m = SHEBANG_RE.match(data)
|
| 300 |
+
if m:
|
| 301 |
+
end = m.end()
|
| 302 |
+
shebang, data_after_shebang = data[:end], data[end:]
|
| 303 |
+
# Preserve any arguments after the interpreter
|
| 304 |
+
if b'pythonw' in shebang.lower():
|
| 305 |
+
shebang_python = SHEBANG_PYTHONW
|
| 306 |
+
else:
|
| 307 |
+
shebang_python = SHEBANG_PYTHON
|
| 308 |
+
m = SHEBANG_DETAIL_RE.match(shebang)
|
| 309 |
+
if m:
|
| 310 |
+
args = b' ' + m.groups()[-1]
|
| 311 |
+
else:
|
| 312 |
+
args = b''
|
| 313 |
+
shebang = shebang_python + args
|
| 314 |
+
data = shebang + data_after_shebang
|
| 315 |
+
else:
|
| 316 |
+
cr = data.find(b'\r')
|
| 317 |
+
lf = data.find(b'\n')
|
| 318 |
+
if cr < 0 or cr > lf:
|
| 319 |
+
term = b'\n'
|
| 320 |
+
else:
|
| 321 |
+
if data[cr:cr + 2] == b'\r\n':
|
| 322 |
+
term = b'\r\n'
|
| 323 |
+
else:
|
| 324 |
+
term = b'\r'
|
| 325 |
+
data = SHEBANG_PYTHON + term + data
|
| 326 |
+
return data
|
| 327 |
+
|
| 328 |
+
def get_hash(self, data, hash_kind=None):
|
| 329 |
+
if hash_kind is None:
|
| 330 |
+
hash_kind = self.hash_kind
|
| 331 |
+
try:
|
| 332 |
+
hasher = getattr(hashlib, hash_kind)
|
| 333 |
+
except AttributeError:
|
| 334 |
+
raise DistlibException('Unsupported hash algorithm: %r' % hash_kind)
|
| 335 |
+
result = hasher(data).digest()
|
| 336 |
+
result = base64.urlsafe_b64encode(result).rstrip(b'=').decode('ascii')
|
| 337 |
+
return hash_kind, result
|
| 338 |
+
|
| 339 |
+
def write_record(self, records, record_path, archive_record_path):
|
| 340 |
+
records = list(records) # make a copy, as mutated
|
| 341 |
+
records.append((archive_record_path, '', ''))
|
| 342 |
+
with CSVWriter(record_path) as writer:
|
| 343 |
+
for row in records:
|
| 344 |
+
writer.writerow(row)
|
| 345 |
+
|
| 346 |
+
def write_records(self, info, libdir, archive_paths):
|
| 347 |
+
records = []
|
| 348 |
+
distinfo, info_dir = info
|
| 349 |
+
# hasher = getattr(hashlib, self.hash_kind)
|
| 350 |
+
for ap, p in archive_paths:
|
| 351 |
+
with open(p, 'rb') as f:
|
| 352 |
+
data = f.read()
|
| 353 |
+
digest = '%s=%s' % self.get_hash(data)
|
| 354 |
+
size = os.path.getsize(p)
|
| 355 |
+
records.append((ap, digest, size))
|
| 356 |
+
|
| 357 |
+
p = os.path.join(distinfo, 'RECORD')
|
| 358 |
+
ap = to_posix(os.path.join(info_dir, 'RECORD'))
|
| 359 |
+
self.write_record(records, p, ap)
|
| 360 |
+
archive_paths.append((ap, p))
|
| 361 |
+
|
| 362 |
+
def build_zip(self, pathname, archive_paths):
|
| 363 |
+
with ZipFile(pathname, 'w', zipfile.ZIP_DEFLATED) as zf:
|
| 364 |
+
for ap, p in archive_paths:
|
| 365 |
+
logger.debug('Wrote %s to %s in wheel', p, ap)
|
| 366 |
+
zf.write(p, ap)
|
| 367 |
+
|
| 368 |
+
def build(self, paths, tags=None, wheel_version=None):
|
| 369 |
+
"""
|
| 370 |
+
Build a wheel from files in specified paths, and use any specified tags
|
| 371 |
+
when determining the name of the wheel.
|
| 372 |
+
"""
|
| 373 |
+
if tags is None:
|
| 374 |
+
tags = {}
|
| 375 |
+
|
| 376 |
+
libkey = list(filter(lambda o: o in paths, ('purelib', 'platlib')))[0]
|
| 377 |
+
if libkey == 'platlib':
|
| 378 |
+
is_pure = 'false'
|
| 379 |
+
default_pyver = [IMPVER]
|
| 380 |
+
default_abi = [ABI]
|
| 381 |
+
default_arch = [ARCH]
|
| 382 |
+
else:
|
| 383 |
+
is_pure = 'true'
|
| 384 |
+
default_pyver = [PYVER]
|
| 385 |
+
default_abi = ['none']
|
| 386 |
+
default_arch = ['any']
|
| 387 |
+
|
| 388 |
+
self.pyver = tags.get('pyver', default_pyver)
|
| 389 |
+
self.abi = tags.get('abi', default_abi)
|
| 390 |
+
self.arch = tags.get('arch', default_arch)
|
| 391 |
+
|
| 392 |
+
libdir = paths[libkey]
|
| 393 |
+
|
| 394 |
+
name_ver = '%s-%s' % (self.name, self.version)
|
| 395 |
+
data_dir = '%s.data' % name_ver
|
| 396 |
+
info_dir = '%s.dist-info' % name_ver
|
| 397 |
+
|
| 398 |
+
archive_paths = []
|
| 399 |
+
|
| 400 |
+
# First, stuff which is not in site-packages
|
| 401 |
+
for key in ('data', 'headers', 'scripts'):
|
| 402 |
+
if key not in paths:
|
| 403 |
+
continue
|
| 404 |
+
path = paths[key]
|
| 405 |
+
if os.path.isdir(path):
|
| 406 |
+
for root, dirs, files in os.walk(path):
|
| 407 |
+
for fn in files:
|
| 408 |
+
p = fsdecode(os.path.join(root, fn))
|
| 409 |
+
rp = os.path.relpath(p, path)
|
| 410 |
+
ap = to_posix(os.path.join(data_dir, key, rp))
|
| 411 |
+
archive_paths.append((ap, p))
|
| 412 |
+
if key == 'scripts' and not p.endswith('.exe'):
|
| 413 |
+
with open(p, 'rb') as f:
|
| 414 |
+
data = f.read()
|
| 415 |
+
data = self.process_shebang(data)
|
| 416 |
+
with open(p, 'wb') as f:
|
| 417 |
+
f.write(data)
|
| 418 |
+
|
| 419 |
+
# Now, stuff which is in site-packages, other than the
|
| 420 |
+
# distinfo stuff.
|
| 421 |
+
path = libdir
|
| 422 |
+
distinfo = None
|
| 423 |
+
for root, dirs, files in os.walk(path):
|
| 424 |
+
if root == path:
|
| 425 |
+
# At the top level only, save distinfo for later
|
| 426 |
+
# and skip it for now
|
| 427 |
+
for i, dn in enumerate(dirs):
|
| 428 |
+
dn = fsdecode(dn)
|
| 429 |
+
if dn.endswith('.dist-info'):
|
| 430 |
+
distinfo = os.path.join(root, dn)
|
| 431 |
+
del dirs[i]
|
| 432 |
+
break
|
| 433 |
+
assert distinfo, '.dist-info directory expected, not found'
|
| 434 |
+
|
| 435 |
+
for fn in files:
|
| 436 |
+
# comment out next suite to leave .pyc files in
|
| 437 |
+
if fsdecode(fn).endswith(('.pyc', '.pyo')):
|
| 438 |
+
continue
|
| 439 |
+
p = os.path.join(root, fn)
|
| 440 |
+
rp = to_posix(os.path.relpath(p, path))
|
| 441 |
+
archive_paths.append((rp, p))
|
| 442 |
+
|
| 443 |
+
# Now distinfo. Assumed to be flat, i.e. os.listdir is enough.
|
| 444 |
+
files = os.listdir(distinfo)
|
| 445 |
+
for fn in files:
|
| 446 |
+
if fn not in ('RECORD', 'INSTALLER', 'SHARED', 'WHEEL'):
|
| 447 |
+
p = fsdecode(os.path.join(distinfo, fn))
|
| 448 |
+
ap = to_posix(os.path.join(info_dir, fn))
|
| 449 |
+
archive_paths.append((ap, p))
|
| 450 |
+
|
| 451 |
+
wheel_metadata = [
|
| 452 |
+
'Wheel-Version: %d.%d' % (wheel_version or self.wheel_version),
|
| 453 |
+
'Generator: distlib %s' % __version__,
|
| 454 |
+
'Root-Is-Purelib: %s' % is_pure,
|
| 455 |
+
]
|
| 456 |
+
for pyver, abi, arch in self.tags:
|
| 457 |
+
wheel_metadata.append('Tag: %s-%s-%s' % (pyver, abi, arch))
|
| 458 |
+
p = os.path.join(distinfo, 'WHEEL')
|
| 459 |
+
with open(p, 'w') as f:
|
| 460 |
+
f.write('\n'.join(wheel_metadata))
|
| 461 |
+
ap = to_posix(os.path.join(info_dir, 'WHEEL'))
|
| 462 |
+
archive_paths.append((ap, p))
|
| 463 |
+
|
| 464 |
+
# sort the entries by archive path. Not needed by any spec, but it
|
| 465 |
+
# keeps the archive listing and RECORD tidier than they would otherwise
|
| 466 |
+
# be. Use the number of path segments to keep directory entries together,
|
| 467 |
+
# and keep the dist-info stuff at the end.
|
| 468 |
+
def sorter(t):
|
| 469 |
+
ap = t[0]
|
| 470 |
+
n = ap.count('/')
|
| 471 |
+
if '.dist-info' in ap:
|
| 472 |
+
n += 10000
|
| 473 |
+
return (n, ap)
|
| 474 |
+
|
| 475 |
+
archive_paths = sorted(archive_paths, key=sorter)
|
| 476 |
+
|
| 477 |
+
# Now, at last, RECORD.
|
| 478 |
+
# Paths in here are archive paths - nothing else makes sense.
|
| 479 |
+
self.write_records((distinfo, info_dir), libdir, archive_paths)
|
| 480 |
+
# Now, ready to build the zip file
|
| 481 |
+
pathname = os.path.join(self.dirname, self.filename)
|
| 482 |
+
self.build_zip(pathname, archive_paths)
|
| 483 |
+
return pathname
|
| 484 |
+
|
| 485 |
+
def skip_entry(self, arcname):
|
| 486 |
+
"""
|
| 487 |
+
Determine whether an archive entry should be skipped when verifying
|
| 488 |
+
or installing.
|
| 489 |
+
"""
|
| 490 |
+
# The signature file won't be in RECORD,
|
| 491 |
+
# and we don't currently don't do anything with it
|
| 492 |
+
# We also skip directories, as they won't be in RECORD
|
| 493 |
+
# either. See:
|
| 494 |
+
#
|
| 495 |
+
# https://github.com/pypa/wheel/issues/294
|
| 496 |
+
# https://github.com/pypa/wheel/issues/287
|
| 497 |
+
# https://github.com/pypa/wheel/pull/289
|
| 498 |
+
#
|
| 499 |
+
return arcname.endswith(('/', '/RECORD.jws'))
|
| 500 |
+
|
| 501 |
+
def install(self, paths, maker, **kwargs):
|
| 502 |
+
"""
|
| 503 |
+
Install a wheel to the specified paths. If kwarg ``warner`` is
|
| 504 |
+
specified, it should be a callable, which will be called with two
|
| 505 |
+
tuples indicating the wheel version of this software and the wheel
|
| 506 |
+
version in the file, if there is a discrepancy in the versions.
|
| 507 |
+
This can be used to issue any warnings to raise any exceptions.
|
| 508 |
+
If kwarg ``lib_only`` is True, only the purelib/platlib files are
|
| 509 |
+
installed, and the headers, scripts, data and dist-info metadata are
|
| 510 |
+
not written. If kwarg ``bytecode_hashed_invalidation`` is True, written
|
| 511 |
+
bytecode will try to use file-hash based invalidation (PEP-552) on
|
| 512 |
+
supported interpreter versions (CPython 3.7+).
|
| 513 |
+
|
| 514 |
+
The return value is a :class:`InstalledDistribution` instance unless
|
| 515 |
+
``options.lib_only`` is True, in which case the return value is ``None``.
|
| 516 |
+
"""
|
| 517 |
+
|
| 518 |
+
dry_run = maker.dry_run
|
| 519 |
+
warner = kwargs.get('warner')
|
| 520 |
+
lib_only = kwargs.get('lib_only', False)
|
| 521 |
+
bc_hashed_invalidation = kwargs.get('bytecode_hashed_invalidation', False)
|
| 522 |
+
|
| 523 |
+
pathname = os.path.join(self.dirname, self.filename)
|
| 524 |
+
name_ver = '%s-%s' % (self.name, self.version)
|
| 525 |
+
data_dir = '%s.data' % name_ver
|
| 526 |
+
info_dir = '%s.dist-info' % name_ver
|
| 527 |
+
|
| 528 |
+
metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)
|
| 529 |
+
wheel_metadata_name = posixpath.join(info_dir, 'WHEEL')
|
| 530 |
+
record_name = posixpath.join(info_dir, 'RECORD')
|
| 531 |
+
|
| 532 |
+
wrapper = codecs.getreader('utf-8')
|
| 533 |
+
|
| 534 |
+
with ZipFile(pathname, 'r') as zf:
|
| 535 |
+
with zf.open(wheel_metadata_name) as bwf:
|
| 536 |
+
wf = wrapper(bwf)
|
| 537 |
+
message = message_from_file(wf)
|
| 538 |
+
wv = message['Wheel-Version'].split('.', 1)
|
| 539 |
+
file_version = tuple([int(i) for i in wv])
|
| 540 |
+
if (file_version != self.wheel_version) and warner:
|
| 541 |
+
warner(self.wheel_version, file_version)
|
| 542 |
+
|
| 543 |
+
if message['Root-Is-Purelib'] == 'true':
|
| 544 |
+
libdir = paths['purelib']
|
| 545 |
+
else:
|
| 546 |
+
libdir = paths['platlib']
|
| 547 |
+
|
| 548 |
+
records = {}
|
| 549 |
+
with zf.open(record_name) as bf:
|
| 550 |
+
with CSVReader(stream=bf) as reader:
|
| 551 |
+
for row in reader:
|
| 552 |
+
p = row[0]
|
| 553 |
+
records[p] = row
|
| 554 |
+
|
| 555 |
+
data_pfx = posixpath.join(data_dir, '')
|
| 556 |
+
info_pfx = posixpath.join(info_dir, '')
|
| 557 |
+
script_pfx = posixpath.join(data_dir, 'scripts', '')
|
| 558 |
+
|
| 559 |
+
# make a new instance rather than a copy of maker's,
|
| 560 |
+
# as we mutate it
|
| 561 |
+
fileop = FileOperator(dry_run=dry_run)
|
| 562 |
+
fileop.record = True # so we can rollback if needed
|
| 563 |
+
|
| 564 |
+
bc = not sys.dont_write_bytecode # Double negatives. Lovely!
|
| 565 |
+
|
| 566 |
+
outfiles = [] # for RECORD writing
|
| 567 |
+
|
| 568 |
+
# for script copying/shebang processing
|
| 569 |
+
workdir = tempfile.mkdtemp()
|
| 570 |
+
# set target dir later
|
| 571 |
+
# we default add_launchers to False, as the
|
| 572 |
+
# Python Launcher should be used instead
|
| 573 |
+
maker.source_dir = workdir
|
| 574 |
+
maker.target_dir = None
|
| 575 |
+
try:
|
| 576 |
+
for zinfo in zf.infolist():
|
| 577 |
+
arcname = zinfo.filename
|
| 578 |
+
if isinstance(arcname, text_type):
|
| 579 |
+
u_arcname = arcname
|
| 580 |
+
else:
|
| 581 |
+
u_arcname = arcname.decode('utf-8')
|
| 582 |
+
if self.skip_entry(u_arcname):
|
| 583 |
+
continue
|
| 584 |
+
row = records[u_arcname]
|
| 585 |
+
if row[2] and str(zinfo.file_size) != row[2]:
|
| 586 |
+
raise DistlibException('size mismatch for '
|
| 587 |
+
'%s' % u_arcname)
|
| 588 |
+
if row[1]:
|
| 589 |
+
kind, value = row[1].split('=', 1)
|
| 590 |
+
with zf.open(arcname) as bf:
|
| 591 |
+
data = bf.read()
|
| 592 |
+
_, digest = self.get_hash(data, kind)
|
| 593 |
+
if digest != value:
|
| 594 |
+
raise DistlibException('digest mismatch for '
|
| 595 |
+
'%s' % arcname)
|
| 596 |
+
|
| 597 |
+
if lib_only and u_arcname.startswith((info_pfx, data_pfx)):
|
| 598 |
+
logger.debug('lib_only: skipping %s', u_arcname)
|
| 599 |
+
continue
|
| 600 |
+
is_script = (u_arcname.startswith(script_pfx) and not u_arcname.endswith('.exe'))
|
| 601 |
+
|
| 602 |
+
if u_arcname.startswith(data_pfx):
|
| 603 |
+
_, where, rp = u_arcname.split('/', 2)
|
| 604 |
+
outfile = os.path.join(paths[where], convert_path(rp))
|
| 605 |
+
else:
|
| 606 |
+
# meant for site-packages.
|
| 607 |
+
if u_arcname in (wheel_metadata_name, record_name):
|
| 608 |
+
continue
|
| 609 |
+
outfile = os.path.join(libdir, convert_path(u_arcname))
|
| 610 |
+
if not is_script:
|
| 611 |
+
with zf.open(arcname) as bf:
|
| 612 |
+
fileop.copy_stream(bf, outfile)
|
| 613 |
+
# Issue #147: permission bits aren't preserved. Using
|
| 614 |
+
# zf.extract(zinfo, libdir) should have worked, but didn't,
|
| 615 |
+
# see https://www.thetopsites.net/article/53834422.shtml
|
| 616 |
+
# So ... manually preserve permission bits as given in zinfo
|
| 617 |
+
if os.name == 'posix':
|
| 618 |
+
# just set the normal permission bits
|
| 619 |
+
os.chmod(outfile, (zinfo.external_attr >> 16) & 0x1FF)
|
| 620 |
+
outfiles.append(outfile)
|
| 621 |
+
# Double check the digest of the written file
|
| 622 |
+
if not dry_run and row[1]:
|
| 623 |
+
with open(outfile, 'rb') as bf:
|
| 624 |
+
data = bf.read()
|
| 625 |
+
_, newdigest = self.get_hash(data, kind)
|
| 626 |
+
if newdigest != digest:
|
| 627 |
+
raise DistlibException('digest mismatch '
|
| 628 |
+
'on write for '
|
| 629 |
+
'%s' % outfile)
|
| 630 |
+
if bc and outfile.endswith('.py'):
|
| 631 |
+
try:
|
| 632 |
+
pyc = fileop.byte_compile(outfile, hashed_invalidation=bc_hashed_invalidation)
|
| 633 |
+
outfiles.append(pyc)
|
| 634 |
+
except Exception:
|
| 635 |
+
# Don't give up if byte-compilation fails,
|
| 636 |
+
# but log it and perhaps warn the user
|
| 637 |
+
logger.warning('Byte-compilation failed', exc_info=True)
|
| 638 |
+
else:
|
| 639 |
+
fn = os.path.basename(convert_path(arcname))
|
| 640 |
+
workname = os.path.join(workdir, fn)
|
| 641 |
+
with zf.open(arcname) as bf:
|
| 642 |
+
fileop.copy_stream(bf, workname)
|
| 643 |
+
|
| 644 |
+
dn, fn = os.path.split(outfile)
|
| 645 |
+
maker.target_dir = dn
|
| 646 |
+
filenames = maker.make(fn)
|
| 647 |
+
fileop.set_executable_mode(filenames)
|
| 648 |
+
outfiles.extend(filenames)
|
| 649 |
+
|
| 650 |
+
if lib_only:
|
| 651 |
+
logger.debug('lib_only: returning None')
|
| 652 |
+
dist = None
|
| 653 |
+
else:
|
| 654 |
+
# Generate scripts
|
| 655 |
+
|
| 656 |
+
# Try to get pydist.json so we can see if there are
|
| 657 |
+
# any commands to generate. If this fails (e.g. because
|
| 658 |
+
# of a legacy wheel), log a warning but don't give up.
|
| 659 |
+
commands = None
|
| 660 |
+
file_version = self.info['Wheel-Version']
|
| 661 |
+
if file_version == '1.0':
|
| 662 |
+
# Use legacy info
|
| 663 |
+
ep = posixpath.join(info_dir, 'entry_points.txt')
|
| 664 |
+
try:
|
| 665 |
+
with zf.open(ep) as bwf:
|
| 666 |
+
epdata = read_exports(bwf)
|
| 667 |
+
commands = {}
|
| 668 |
+
for key in ('console', 'gui'):
|
| 669 |
+
k = '%s_scripts' % key
|
| 670 |
+
if k in epdata:
|
| 671 |
+
commands['wrap_%s' % key] = d = {}
|
| 672 |
+
for v in epdata[k].values():
|
| 673 |
+
s = '%s:%s' % (v.prefix, v.suffix)
|
| 674 |
+
if v.flags:
|
| 675 |
+
s += ' [%s]' % ','.join(v.flags)
|
| 676 |
+
d[v.name] = s
|
| 677 |
+
except Exception:
|
| 678 |
+
logger.warning('Unable to read legacy script '
|
| 679 |
+
'metadata, so cannot generate '
|
| 680 |
+
'scripts')
|
| 681 |
+
else:
|
| 682 |
+
try:
|
| 683 |
+
with zf.open(metadata_name) as bwf:
|
| 684 |
+
wf = wrapper(bwf)
|
| 685 |
+
commands = json.load(wf).get('extensions')
|
| 686 |
+
if commands:
|
| 687 |
+
commands = commands.get('python.commands')
|
| 688 |
+
except Exception:
|
| 689 |
+
logger.warning('Unable to read JSON metadata, so '
|
| 690 |
+
'cannot generate scripts')
|
| 691 |
+
if commands:
|
| 692 |
+
console_scripts = commands.get('wrap_console', {})
|
| 693 |
+
gui_scripts = commands.get('wrap_gui', {})
|
| 694 |
+
if console_scripts or gui_scripts:
|
| 695 |
+
script_dir = paths.get('scripts', '')
|
| 696 |
+
if not os.path.isdir(script_dir):
|
| 697 |
+
raise ValueError('Valid script path not '
|
| 698 |
+
'specified')
|
| 699 |
+
maker.target_dir = script_dir
|
| 700 |
+
for k, v in console_scripts.items():
|
| 701 |
+
script = '%s = %s' % (k, v)
|
| 702 |
+
filenames = maker.make(script)
|
| 703 |
+
fileop.set_executable_mode(filenames)
|
| 704 |
+
|
| 705 |
+
if gui_scripts:
|
| 706 |
+
options = {'gui': True}
|
| 707 |
+
for k, v in gui_scripts.items():
|
| 708 |
+
script = '%s = %s' % (k, v)
|
| 709 |
+
filenames = maker.make(script, options)
|
| 710 |
+
fileop.set_executable_mode(filenames)
|
| 711 |
+
|
| 712 |
+
p = os.path.join(libdir, info_dir)
|
| 713 |
+
dist = InstalledDistribution(p)
|
| 714 |
+
|
| 715 |
+
# Write SHARED
|
| 716 |
+
paths = dict(paths) # don't change passed in dict
|
| 717 |
+
del paths['purelib']
|
| 718 |
+
del paths['platlib']
|
| 719 |
+
paths['lib'] = libdir
|
| 720 |
+
p = dist.write_shared_locations(paths, dry_run)
|
| 721 |
+
if p:
|
| 722 |
+
outfiles.append(p)
|
| 723 |
+
|
| 724 |
+
# Write RECORD
|
| 725 |
+
dist.write_installed_files(outfiles, paths['prefix'], dry_run)
|
| 726 |
+
return dist
|
| 727 |
+
except Exception: # pragma: no cover
|
| 728 |
+
logger.exception('installation failed.')
|
| 729 |
+
fileop.rollback()
|
| 730 |
+
raise
|
| 731 |
+
finally:
|
| 732 |
+
shutil.rmtree(workdir)
|
| 733 |
+
|
| 734 |
+
def _get_dylib_cache(self):
|
| 735 |
+
global cache
|
| 736 |
+
if cache is None:
|
| 737 |
+
# Use native string to avoid issues on 2.x: see Python #20140.
|
| 738 |
+
base = os.path.join(get_cache_base(), str('dylib-cache'), '%s.%s' % sys.version_info[:2])
|
| 739 |
+
cache = Cache(base)
|
| 740 |
+
return cache
|
| 741 |
+
|
| 742 |
+
def _get_extensions(self):
|
| 743 |
+
pathname = os.path.join(self.dirname, self.filename)
|
| 744 |
+
name_ver = '%s-%s' % (self.name, self.version)
|
| 745 |
+
info_dir = '%s.dist-info' % name_ver
|
| 746 |
+
arcname = posixpath.join(info_dir, 'EXTENSIONS')
|
| 747 |
+
wrapper = codecs.getreader('utf-8')
|
| 748 |
+
result = []
|
| 749 |
+
with ZipFile(pathname, 'r') as zf:
|
| 750 |
+
try:
|
| 751 |
+
with zf.open(arcname) as bf:
|
| 752 |
+
wf = wrapper(bf)
|
| 753 |
+
extensions = json.load(wf)
|
| 754 |
+
cache = self._get_dylib_cache()
|
| 755 |
+
prefix = cache.prefix_to_dir(self.filename, use_abspath=False)
|
| 756 |
+
cache_base = os.path.join(cache.base, prefix)
|
| 757 |
+
if not os.path.isdir(cache_base):
|
| 758 |
+
os.makedirs(cache_base)
|
| 759 |
+
for name, relpath in extensions.items():
|
| 760 |
+
dest = os.path.join(cache_base, convert_path(relpath))
|
| 761 |
+
if not os.path.exists(dest):
|
| 762 |
+
extract = True
|
| 763 |
+
else:
|
| 764 |
+
file_time = os.stat(dest).st_mtime
|
| 765 |
+
file_time = datetime.datetime.fromtimestamp(file_time)
|
| 766 |
+
info = zf.getinfo(relpath)
|
| 767 |
+
wheel_time = datetime.datetime(*info.date_time)
|
| 768 |
+
extract = wheel_time > file_time
|
| 769 |
+
if extract:
|
| 770 |
+
zf.extract(relpath, cache_base)
|
| 771 |
+
result.append((name, dest))
|
| 772 |
+
except KeyError:
|
| 773 |
+
pass
|
| 774 |
+
return result
|
| 775 |
+
|
| 776 |
+
def is_compatible(self):
|
| 777 |
+
"""
|
| 778 |
+
Determine if a wheel is compatible with the running system.
|
| 779 |
+
"""
|
| 780 |
+
return is_compatible(self)
|
| 781 |
+
|
| 782 |
+
def is_mountable(self):
|
| 783 |
+
"""
|
| 784 |
+
Determine if a wheel is asserted as mountable by its metadata.
|
| 785 |
+
"""
|
| 786 |
+
return True # for now - metadata details TBD
|
| 787 |
+
|
| 788 |
+
def mount(self, append=False):
|
| 789 |
+
pathname = os.path.abspath(os.path.join(self.dirname, self.filename))
|
| 790 |
+
if not self.is_compatible():
|
| 791 |
+
msg = 'Wheel %s not compatible with this Python.' % pathname
|
| 792 |
+
raise DistlibException(msg)
|
| 793 |
+
if not self.is_mountable():
|
| 794 |
+
msg = 'Wheel %s is marked as not mountable.' % pathname
|
| 795 |
+
raise DistlibException(msg)
|
| 796 |
+
if pathname in sys.path:
|
| 797 |
+
logger.debug('%s already in path', pathname)
|
| 798 |
+
else:
|
| 799 |
+
if append:
|
| 800 |
+
sys.path.append(pathname)
|
| 801 |
+
else:
|
| 802 |
+
sys.path.insert(0, pathname)
|
| 803 |
+
extensions = self._get_extensions()
|
| 804 |
+
if extensions:
|
| 805 |
+
if _hook not in sys.meta_path:
|
| 806 |
+
sys.meta_path.append(_hook)
|
| 807 |
+
_hook.add(pathname, extensions)
|
| 808 |
+
|
| 809 |
+
def unmount(self):
|
| 810 |
+
pathname = os.path.abspath(os.path.join(self.dirname, self.filename))
|
| 811 |
+
if pathname not in sys.path:
|
| 812 |
+
logger.debug('%s not in path', pathname)
|
| 813 |
+
else:
|
| 814 |
+
sys.path.remove(pathname)
|
| 815 |
+
if pathname in _hook.impure_wheels:
|
| 816 |
+
_hook.remove(pathname)
|
| 817 |
+
if not _hook.impure_wheels:
|
| 818 |
+
if _hook in sys.meta_path:
|
| 819 |
+
sys.meta_path.remove(_hook)
|
| 820 |
+
|
| 821 |
+
def verify(self):
|
| 822 |
+
pathname = os.path.join(self.dirname, self.filename)
|
| 823 |
+
name_ver = '%s-%s' % (self.name, self.version)
|
| 824 |
+
# data_dir = '%s.data' % name_ver
|
| 825 |
+
info_dir = '%s.dist-info' % name_ver
|
| 826 |
+
|
| 827 |
+
# metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)
|
| 828 |
+
wheel_metadata_name = posixpath.join(info_dir, 'WHEEL')
|
| 829 |
+
record_name = posixpath.join(info_dir, 'RECORD')
|
| 830 |
+
|
| 831 |
+
wrapper = codecs.getreader('utf-8')
|
| 832 |
+
|
| 833 |
+
with ZipFile(pathname, 'r') as zf:
|
| 834 |
+
with zf.open(wheel_metadata_name) as bwf:
|
| 835 |
+
wf = wrapper(bwf)
|
| 836 |
+
message_from_file(wf)
|
| 837 |
+
# wv = message['Wheel-Version'].split('.', 1)
|
| 838 |
+
# file_version = tuple([int(i) for i in wv])
|
| 839 |
+
# TODO version verification
|
| 840 |
+
|
| 841 |
+
records = {}
|
| 842 |
+
with zf.open(record_name) as bf:
|
| 843 |
+
with CSVReader(stream=bf) as reader:
|
| 844 |
+
for row in reader:
|
| 845 |
+
p = row[0]
|
| 846 |
+
records[p] = row
|
| 847 |
+
|
| 848 |
+
for zinfo in zf.infolist():
|
| 849 |
+
arcname = zinfo.filename
|
| 850 |
+
if isinstance(arcname, text_type):
|
| 851 |
+
u_arcname = arcname
|
| 852 |
+
else:
|
| 853 |
+
u_arcname = arcname.decode('utf-8')
|
| 854 |
+
# See issue #115: some wheels have .. in their entries, but
|
| 855 |
+
# in the filename ... e.g. __main__..py ! So the check is
|
| 856 |
+
# updated to look for .. in the directory portions
|
| 857 |
+
p = u_arcname.split('/')
|
| 858 |
+
if '..' in p:
|
| 859 |
+
raise DistlibException('invalid entry in '
|
| 860 |
+
'wheel: %r' % u_arcname)
|
| 861 |
+
|
| 862 |
+
if self.skip_entry(u_arcname):
|
| 863 |
+
continue
|
| 864 |
+
row = records[u_arcname]
|
| 865 |
+
if row[2] and str(zinfo.file_size) != row[2]:
|
| 866 |
+
raise DistlibException('size mismatch for '
|
| 867 |
+
'%s' % u_arcname)
|
| 868 |
+
if row[1]:
|
| 869 |
+
kind, value = row[1].split('=', 1)
|
| 870 |
+
with zf.open(arcname) as bf:
|
| 871 |
+
data = bf.read()
|
| 872 |
+
_, digest = self.get_hash(data, kind)
|
| 873 |
+
if digest != value:
|
| 874 |
+
raise DistlibException('digest mismatch for '
|
| 875 |
+
'%s' % arcname)
|
| 876 |
+
|
| 877 |
+
def update(self, modifier, dest_dir=None, **kwargs):
|
| 878 |
+
"""
|
| 879 |
+
Update the contents of a wheel in a generic way. The modifier should
|
| 880 |
+
be a callable which expects a dictionary argument: its keys are
|
| 881 |
+
archive-entry paths, and its values are absolute filesystem paths
|
| 882 |
+
where the contents the corresponding archive entries can be found. The
|
| 883 |
+
modifier is free to change the contents of the files pointed to, add
|
| 884 |
+
new entries and remove entries, before returning. This method will
|
| 885 |
+
extract the entire contents of the wheel to a temporary location, call
|
| 886 |
+
the modifier, and then use the passed (and possibly updated)
|
| 887 |
+
dictionary to write a new wheel. If ``dest_dir`` is specified, the new
|
| 888 |
+
wheel is written there -- otherwise, the original wheel is overwritten.
|
| 889 |
+
|
| 890 |
+
The modifier should return True if it updated the wheel, else False.
|
| 891 |
+
This method returns the same value the modifier returns.
|
| 892 |
+
"""
|
| 893 |
+
|
| 894 |
+
def get_version(path_map, info_dir):
|
| 895 |
+
version = path = None
|
| 896 |
+
key = '%s/%s' % (info_dir, LEGACY_METADATA_FILENAME)
|
| 897 |
+
if key not in path_map:
|
| 898 |
+
key = '%s/PKG-INFO' % info_dir
|
| 899 |
+
if key in path_map:
|
| 900 |
+
path = path_map[key]
|
| 901 |
+
version = Metadata(path=path).version
|
| 902 |
+
return version, path
|
| 903 |
+
|
| 904 |
+
def update_version(version, path):
|
| 905 |
+
updated = None
|
| 906 |
+
try:
|
| 907 |
+
NormalizedVersion(version)
|
| 908 |
+
i = version.find('-')
|
| 909 |
+
if i < 0:
|
| 910 |
+
updated = '%s+1' % version
|
| 911 |
+
else:
|
| 912 |
+
parts = [int(s) for s in version[i + 1:].split('.')]
|
| 913 |
+
parts[-1] += 1
|
| 914 |
+
updated = '%s+%s' % (version[:i], '.'.join(str(i) for i in parts))
|
| 915 |
+
except UnsupportedVersionError:
|
| 916 |
+
logger.debug('Cannot update non-compliant (PEP-440) '
|
| 917 |
+
'version %r', version)
|
| 918 |
+
if updated:
|
| 919 |
+
md = Metadata(path=path)
|
| 920 |
+
md.version = updated
|
| 921 |
+
legacy = path.endswith(LEGACY_METADATA_FILENAME)
|
| 922 |
+
md.write(path=path, legacy=legacy)
|
| 923 |
+
logger.debug('Version updated from %r to %r', version, updated)
|
| 924 |
+
|
| 925 |
+
pathname = os.path.join(self.dirname, self.filename)
|
| 926 |
+
name_ver = '%s-%s' % (self.name, self.version)
|
| 927 |
+
info_dir = '%s.dist-info' % name_ver
|
| 928 |
+
record_name = posixpath.join(info_dir, 'RECORD')
|
| 929 |
+
with tempdir() as workdir:
|
| 930 |
+
with ZipFile(pathname, 'r') as zf:
|
| 931 |
+
path_map = {}
|
| 932 |
+
for zinfo in zf.infolist():
|
| 933 |
+
arcname = zinfo.filename
|
| 934 |
+
if isinstance(arcname, text_type):
|
| 935 |
+
u_arcname = arcname
|
| 936 |
+
else:
|
| 937 |
+
u_arcname = arcname.decode('utf-8')
|
| 938 |
+
if u_arcname == record_name:
|
| 939 |
+
continue
|
| 940 |
+
if '..' in u_arcname:
|
| 941 |
+
raise DistlibException('invalid entry in '
|
| 942 |
+
'wheel: %r' % u_arcname)
|
| 943 |
+
zf.extract(zinfo, workdir)
|
| 944 |
+
path = os.path.join(workdir, convert_path(u_arcname))
|
| 945 |
+
path_map[u_arcname] = path
|
| 946 |
+
|
| 947 |
+
# Remember the version.
|
| 948 |
+
original_version, _ = get_version(path_map, info_dir)
|
| 949 |
+
# Files extracted. Call the modifier.
|
| 950 |
+
modified = modifier(path_map, **kwargs)
|
| 951 |
+
if modified:
|
| 952 |
+
# Something changed - need to build a new wheel.
|
| 953 |
+
current_version, path = get_version(path_map, info_dir)
|
| 954 |
+
if current_version and (current_version == original_version):
|
| 955 |
+
# Add or update local version to signify changes.
|
| 956 |
+
update_version(current_version, path)
|
| 957 |
+
# Decide where the new wheel goes.
|
| 958 |
+
if dest_dir is None:
|
| 959 |
+
fd, newpath = tempfile.mkstemp(suffix='.whl', prefix='wheel-update-', dir=workdir)
|
| 960 |
+
os.close(fd)
|
| 961 |
+
else:
|
| 962 |
+
if not os.path.isdir(dest_dir):
|
| 963 |
+
raise DistlibException('Not a directory: %r' % dest_dir)
|
| 964 |
+
newpath = os.path.join(dest_dir, self.filename)
|
| 965 |
+
archive_paths = list(path_map.items())
|
| 966 |
+
distinfo = os.path.join(workdir, info_dir)
|
| 967 |
+
info = distinfo, info_dir
|
| 968 |
+
self.write_records(info, workdir, archive_paths)
|
| 969 |
+
self.build_zip(newpath, archive_paths)
|
| 970 |
+
if dest_dir is None:
|
| 971 |
+
shutil.copyfile(newpath, pathname)
|
| 972 |
+
return modified
|
| 973 |
+
|
| 974 |
+
|
| 975 |
+
def _get_glibc_version():
|
| 976 |
+
import platform
|
| 977 |
+
ver = platform.libc_ver()
|
| 978 |
+
result = []
|
| 979 |
+
if ver[0] == 'glibc':
|
| 980 |
+
for s in ver[1].split('.'):
|
| 981 |
+
result.append(int(s) if s.isdigit() else 0)
|
| 982 |
+
result = tuple(result)
|
| 983 |
+
return result
|
| 984 |
+
|
| 985 |
+
|
| 986 |
+
def compatible_tags():
|
| 987 |
+
"""
|
| 988 |
+
Return (pyver, abi, arch) tuples compatible with this Python.
|
| 989 |
+
"""
|
| 990 |
+
class _Version:
|
| 991 |
+
def __init__(self, major, minor):
|
| 992 |
+
self.major = major
|
| 993 |
+
self.major_minor = (major, minor)
|
| 994 |
+
self.string = ''.join((str(major), str(minor)))
|
| 995 |
+
|
| 996 |
+
def __str__(self):
|
| 997 |
+
return self.string
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
versions = [
|
| 1001 |
+
_Version(sys.version_info.major, minor_version)
|
| 1002 |
+
for minor_version in range(sys.version_info.minor, -1, -1)
|
| 1003 |
+
]
|
| 1004 |
+
abis = []
|
| 1005 |
+
for suffix in _get_suffixes():
|
| 1006 |
+
if suffix.startswith('.abi'):
|
| 1007 |
+
abis.append(suffix.split('.', 2)[1])
|
| 1008 |
+
abis.sort()
|
| 1009 |
+
if ABI != 'none':
|
| 1010 |
+
abis.insert(0, ABI)
|
| 1011 |
+
abis.append('none')
|
| 1012 |
+
result = []
|
| 1013 |
+
|
| 1014 |
+
arches = [ARCH]
|
| 1015 |
+
if sys.platform == 'darwin':
|
| 1016 |
+
m = re.match(r'(\w+)_(\d+)_(\d+)_(\w+)$', ARCH)
|
| 1017 |
+
if m:
|
| 1018 |
+
name, major, minor, arch = m.groups()
|
| 1019 |
+
minor = int(minor)
|
| 1020 |
+
matches = [arch]
|
| 1021 |
+
if arch in ('i386', 'ppc'):
|
| 1022 |
+
matches.append('fat')
|
| 1023 |
+
if arch in ('i386', 'ppc', 'x86_64'):
|
| 1024 |
+
matches.append('fat3')
|
| 1025 |
+
if arch in ('ppc64', 'x86_64'):
|
| 1026 |
+
matches.append('fat64')
|
| 1027 |
+
if arch in ('i386', 'x86_64'):
|
| 1028 |
+
matches.append('intel')
|
| 1029 |
+
if arch in ('i386', 'x86_64', 'intel', 'ppc', 'ppc64'):
|
| 1030 |
+
matches.append('universal')
|
| 1031 |
+
while minor >= 0:
|
| 1032 |
+
for match in matches:
|
| 1033 |
+
s = '%s_%s_%s_%s' % (name, major, minor, match)
|
| 1034 |
+
if s != ARCH: # already there
|
| 1035 |
+
arches.append(s)
|
| 1036 |
+
minor -= 1
|
| 1037 |
+
|
| 1038 |
+
# Most specific - our Python version, ABI and arch
|
| 1039 |
+
for i, version_object in enumerate(versions):
|
| 1040 |
+
version = str(version_object)
|
| 1041 |
+
add_abis = []
|
| 1042 |
+
|
| 1043 |
+
if i == 0:
|
| 1044 |
+
add_abis = abis
|
| 1045 |
+
|
| 1046 |
+
if IMP_PREFIX == 'cp' and version_object.major_minor >= (3, 2):
|
| 1047 |
+
limited_api_abi = 'abi' + str(version_object.major)
|
| 1048 |
+
if limited_api_abi not in add_abis:
|
| 1049 |
+
add_abis.append(limited_api_abi)
|
| 1050 |
+
|
| 1051 |
+
for abi in add_abis:
|
| 1052 |
+
for arch in arches:
|
| 1053 |
+
result.append((''.join((IMP_PREFIX, version)), abi, arch))
|
| 1054 |
+
# manylinux
|
| 1055 |
+
if abi != 'none' and sys.platform.startswith('linux'):
|
| 1056 |
+
arch = arch.replace('linux_', '')
|
| 1057 |
+
parts = _get_glibc_version()
|
| 1058 |
+
if len(parts) == 2:
|
| 1059 |
+
if parts >= (2, 5):
|
| 1060 |
+
result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux1_%s' % arch))
|
| 1061 |
+
if parts >= (2, 12):
|
| 1062 |
+
result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux2010_%s' % arch))
|
| 1063 |
+
if parts >= (2, 17):
|
| 1064 |
+
result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux2014_%s' % arch))
|
| 1065 |
+
result.append((''.join(
|
| 1066 |
+
(IMP_PREFIX, version)), abi, 'manylinux_%s_%s_%s' % (parts[0], parts[1], arch)))
|
| 1067 |
+
|
| 1068 |
+
# where no ABI / arch dependency, but IMP_PREFIX dependency
|
| 1069 |
+
for i, version_object in enumerate(versions):
|
| 1070 |
+
version = str(version_object)
|
| 1071 |
+
result.append((''.join((IMP_PREFIX, version)), 'none', 'any'))
|
| 1072 |
+
if i == 0:
|
| 1073 |
+
result.append((''.join((IMP_PREFIX, version[0])), 'none', 'any'))
|
| 1074 |
+
|
| 1075 |
+
# no IMP_PREFIX, ABI or arch dependency
|
| 1076 |
+
for i, version_object in enumerate(versions):
|
| 1077 |
+
version = str(version_object)
|
| 1078 |
+
result.append((''.join(('py', version)), 'none', 'any'))
|
| 1079 |
+
if i == 0:
|
| 1080 |
+
result.append((''.join(('py', version[0])), 'none', 'any'))
|
| 1081 |
+
|
| 1082 |
+
return set(result)
|
| 1083 |
+
|
| 1084 |
+
|
| 1085 |
+
COMPATIBLE_TAGS = compatible_tags()
|
| 1086 |
+
|
| 1087 |
+
del compatible_tags
|
| 1088 |
+
|
| 1089 |
+
|
| 1090 |
+
def is_compatible(wheel, tags=None):
|
| 1091 |
+
if not isinstance(wheel, Wheel):
|
| 1092 |
+
wheel = Wheel(wheel) # assume it's a filename
|
| 1093 |
+
result = False
|
| 1094 |
+
if tags is None:
|
| 1095 |
+
tags = COMPATIBLE_TAGS
|
| 1096 |
+
for ver, abi, arch in tags:
|
| 1097 |
+
if ver in wheel.pyver and abi in wheel.abi and arch in wheel.arch:
|
| 1098 |
+
result = True
|
| 1099 |
+
break
|
| 1100 |
+
return result
|
lib/python3.12/site-packages/pip/_vendor/rich/_export_format.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CONSOLE_HTML_FORMAT = """\
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html>
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<style>
|
| 7 |
+
{stylesheet}
|
| 8 |
+
body {{
|
| 9 |
+
color: {foreground};
|
| 10 |
+
background-color: {background};
|
| 11 |
+
}}
|
| 12 |
+
</style>
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><code style="font-family:inherit">{code}</code></pre>
|
| 16 |
+
</body>
|
| 17 |
+
</html>
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
CONSOLE_SVG_FORMAT = """\
|
| 21 |
+
<svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
|
| 22 |
+
<!-- Generated with Rich https://www.textualize.io -->
|
| 23 |
+
<style>
|
| 24 |
+
|
| 25 |
+
@font-face {{
|
| 26 |
+
font-family: "Fira Code";
|
| 27 |
+
src: local("FiraCode-Regular"),
|
| 28 |
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
| 29 |
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
| 30 |
+
font-style: normal;
|
| 31 |
+
font-weight: 400;
|
| 32 |
+
}}
|
| 33 |
+
@font-face {{
|
| 34 |
+
font-family: "Fira Code";
|
| 35 |
+
src: local("FiraCode-Bold"),
|
| 36 |
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
| 37 |
+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
| 38 |
+
font-style: bold;
|
| 39 |
+
font-weight: 700;
|
| 40 |
+
}}
|
| 41 |
+
|
| 42 |
+
.{unique_id}-matrix {{
|
| 43 |
+
font-family: Fira Code, monospace;
|
| 44 |
+
font-size: {char_height}px;
|
| 45 |
+
line-height: {line_height}px;
|
| 46 |
+
font-variant-east-asian: full-width;
|
| 47 |
+
}}
|
| 48 |
+
|
| 49 |
+
.{unique_id}-title {{
|
| 50 |
+
font-size: 18px;
|
| 51 |
+
font-weight: bold;
|
| 52 |
+
font-family: arial;
|
| 53 |
+
}}
|
| 54 |
+
|
| 55 |
+
{styles}
|
| 56 |
+
</style>
|
| 57 |
+
|
| 58 |
+
<defs>
|
| 59 |
+
<clipPath id="{unique_id}-clip-terminal">
|
| 60 |
+
<rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" />
|
| 61 |
+
</clipPath>
|
| 62 |
+
{lines}
|
| 63 |
+
</defs>
|
| 64 |
+
|
| 65 |
+
{chrome}
|
| 66 |
+
<g transform="translate({terminal_x}, {terminal_y})" clip-path="url(#{unique_id}-clip-terminal)">
|
| 67 |
+
{backgrounds}
|
| 68 |
+
<g class="{unique_id}-matrix">
|
| 69 |
+
{matrix}
|
| 70 |
+
</g>
|
| 71 |
+
</g>
|
| 72 |
+
</svg>
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
_SVG_FONT_FAMILY = "Rich Fira Code"
|
| 76 |
+
_SVG_CLASSES_PREFIX = "rich-svg"
|
lib/python3.12/site-packages/pip/_vendor/rich/table.py
ADDED
|
@@ -0,0 +1,1007 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field, replace
|
| 2 |
+
from typing import (
|
| 3 |
+
TYPE_CHECKING,
|
| 4 |
+
Dict,
|
| 5 |
+
Iterable,
|
| 6 |
+
List,
|
| 7 |
+
NamedTuple,
|
| 8 |
+
Optional,
|
| 9 |
+
Sequence,
|
| 10 |
+
Tuple,
|
| 11 |
+
Union,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
from . import box, errors
|
| 15 |
+
from ._loop import loop_first_last, loop_last
|
| 16 |
+
from ._pick import pick_bool
|
| 17 |
+
from ._ratio import ratio_distribute, ratio_reduce
|
| 18 |
+
from .align import VerticalAlignMethod
|
| 19 |
+
from .jupyter import JupyterMixin
|
| 20 |
+
from .measure import Measurement
|
| 21 |
+
from .padding import Padding, PaddingDimensions
|
| 22 |
+
from .protocol import is_renderable
|
| 23 |
+
from .segment import Segment
|
| 24 |
+
from .style import Style, StyleType
|
| 25 |
+
from .text import Text, TextType
|
| 26 |
+
|
| 27 |
+
if TYPE_CHECKING:
|
| 28 |
+
from .console import (
|
| 29 |
+
Console,
|
| 30 |
+
ConsoleOptions,
|
| 31 |
+
JustifyMethod,
|
| 32 |
+
OverflowMethod,
|
| 33 |
+
RenderableType,
|
| 34 |
+
RenderResult,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class Column:
|
| 40 |
+
"""Defines a column within a ~Table.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
|
| 44 |
+
caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
|
| 45 |
+
width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
|
| 46 |
+
min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
|
| 47 |
+
box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD.
|
| 48 |
+
safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
|
| 49 |
+
padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
|
| 50 |
+
collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
|
| 51 |
+
pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
|
| 52 |
+
expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
|
| 53 |
+
show_header (bool, optional): Show a header row. Defaults to True.
|
| 54 |
+
show_footer (bool, optional): Show a footer row. Defaults to False.
|
| 55 |
+
show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
|
| 56 |
+
show_lines (bool, optional): Draw lines between every row. Defaults to False.
|
| 57 |
+
leading (int, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
|
| 58 |
+
style (Union[str, Style], optional): Default style for the table. Defaults to "none".
|
| 59 |
+
row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None.
|
| 60 |
+
header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
|
| 61 |
+
footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
|
| 62 |
+
border_style (Union[str, Style], optional): Style of the border. Defaults to None.
|
| 63 |
+
title_style (Union[str, Style], optional): Style of the title. Defaults to None.
|
| 64 |
+
caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
|
| 65 |
+
title_justify (str, optional): Justify method for title. Defaults to "center".
|
| 66 |
+
caption_justify (str, optional): Justify method for caption. Defaults to "center".
|
| 67 |
+
highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
header: "RenderableType" = ""
|
| 71 |
+
"""RenderableType: Renderable for the header (typically a string)"""
|
| 72 |
+
|
| 73 |
+
footer: "RenderableType" = ""
|
| 74 |
+
"""RenderableType: Renderable for the footer (typically a string)"""
|
| 75 |
+
|
| 76 |
+
header_style: StyleType = ""
|
| 77 |
+
"""StyleType: The style of the header."""
|
| 78 |
+
|
| 79 |
+
footer_style: StyleType = ""
|
| 80 |
+
"""StyleType: The style of the footer."""
|
| 81 |
+
|
| 82 |
+
style: StyleType = ""
|
| 83 |
+
"""StyleType: The style of the column."""
|
| 84 |
+
|
| 85 |
+
justify: "JustifyMethod" = "left"
|
| 86 |
+
"""str: How to justify text within the column ("left", "center", "right", or "full")"""
|
| 87 |
+
|
| 88 |
+
vertical: "VerticalAlignMethod" = "top"
|
| 89 |
+
"""str: How to vertically align content ("top", "middle", or "bottom")"""
|
| 90 |
+
|
| 91 |
+
overflow: "OverflowMethod" = "ellipsis"
|
| 92 |
+
"""str: Overflow method."""
|
| 93 |
+
|
| 94 |
+
width: Optional[int] = None
|
| 95 |
+
"""Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
|
| 96 |
+
|
| 97 |
+
min_width: Optional[int] = None
|
| 98 |
+
"""Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None."""
|
| 99 |
+
|
| 100 |
+
max_width: Optional[int] = None
|
| 101 |
+
"""Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None."""
|
| 102 |
+
|
| 103 |
+
ratio: Optional[int] = None
|
| 104 |
+
"""Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents."""
|
| 105 |
+
|
| 106 |
+
no_wrap: bool = False
|
| 107 |
+
"""bool: Prevent wrapping of text within the column. Defaults to ``False``."""
|
| 108 |
+
|
| 109 |
+
highlight: bool = False
|
| 110 |
+
"""bool: Apply highlighter to column. Defaults to ``False``."""
|
| 111 |
+
|
| 112 |
+
_index: int = 0
|
| 113 |
+
"""Index of column."""
|
| 114 |
+
|
| 115 |
+
_cells: List["RenderableType"] = field(default_factory=list)
|
| 116 |
+
|
| 117 |
+
def copy(self) -> "Column":
|
| 118 |
+
"""Return a copy of this Column."""
|
| 119 |
+
return replace(self, _cells=[])
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def cells(self) -> Iterable["RenderableType"]:
|
| 123 |
+
"""Get all cells in the column, not including header."""
|
| 124 |
+
yield from self._cells
|
| 125 |
+
|
| 126 |
+
@property
|
| 127 |
+
def flexible(self) -> bool:
|
| 128 |
+
"""Check if this column is flexible."""
|
| 129 |
+
return self.ratio is not None
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@dataclass
|
| 133 |
+
class Row:
|
| 134 |
+
"""Information regarding a row."""
|
| 135 |
+
|
| 136 |
+
style: Optional[StyleType] = None
|
| 137 |
+
"""Style to apply to row."""
|
| 138 |
+
|
| 139 |
+
end_section: bool = False
|
| 140 |
+
"""Indicated end of section, which will force a line beneath the row."""
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class _Cell(NamedTuple):
|
| 144 |
+
"""A single cell in a table."""
|
| 145 |
+
|
| 146 |
+
style: StyleType
|
| 147 |
+
"""Style to apply to cell."""
|
| 148 |
+
renderable: "RenderableType"
|
| 149 |
+
"""Cell renderable."""
|
| 150 |
+
vertical: VerticalAlignMethod
|
| 151 |
+
"""Cell vertical alignment."""
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class Table(JupyterMixin):
|
| 155 |
+
"""A console renderable to draw a table.
|
| 156 |
+
|
| 157 |
+
Args:
|
| 158 |
+
*headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
|
| 159 |
+
title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
|
| 160 |
+
caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
|
| 161 |
+
width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
|
| 162 |
+
min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
|
| 163 |
+
box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD.
|
| 164 |
+
safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
|
| 165 |
+
padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
|
| 166 |
+
collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
|
| 167 |
+
pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
|
| 168 |
+
expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
|
| 169 |
+
show_header (bool, optional): Show a header row. Defaults to True.
|
| 170 |
+
show_footer (bool, optional): Show a footer row. Defaults to False.
|
| 171 |
+
show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
|
| 172 |
+
show_lines (bool, optional): Draw lines between every row. Defaults to False.
|
| 173 |
+
leading (int, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
|
| 174 |
+
style (Union[str, Style], optional): Default style for the table. Defaults to "none".
|
| 175 |
+
row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None.
|
| 176 |
+
header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
|
| 177 |
+
footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
|
| 178 |
+
border_style (Union[str, Style], optional): Style of the border. Defaults to None.
|
| 179 |
+
title_style (Union[str, Style], optional): Style of the title. Defaults to None.
|
| 180 |
+
caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
|
| 181 |
+
title_justify (str, optional): Justify method for title. Defaults to "center".
|
| 182 |
+
caption_justify (str, optional): Justify method for caption. Defaults to "center".
|
| 183 |
+
highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
columns: List[Column]
|
| 187 |
+
rows: List[Row]
|
| 188 |
+
|
| 189 |
+
def __init__(
|
| 190 |
+
self,
|
| 191 |
+
*headers: Union[Column, str],
|
| 192 |
+
title: Optional[TextType] = None,
|
| 193 |
+
caption: Optional[TextType] = None,
|
| 194 |
+
width: Optional[int] = None,
|
| 195 |
+
min_width: Optional[int] = None,
|
| 196 |
+
box: Optional[box.Box] = box.HEAVY_HEAD,
|
| 197 |
+
safe_box: Optional[bool] = None,
|
| 198 |
+
padding: PaddingDimensions = (0, 1),
|
| 199 |
+
collapse_padding: bool = False,
|
| 200 |
+
pad_edge: bool = True,
|
| 201 |
+
expand: bool = False,
|
| 202 |
+
show_header: bool = True,
|
| 203 |
+
show_footer: bool = False,
|
| 204 |
+
show_edge: bool = True,
|
| 205 |
+
show_lines: bool = False,
|
| 206 |
+
leading: int = 0,
|
| 207 |
+
style: StyleType = "none",
|
| 208 |
+
row_styles: Optional[Iterable[StyleType]] = None,
|
| 209 |
+
header_style: Optional[StyleType] = "table.header",
|
| 210 |
+
footer_style: Optional[StyleType] = "table.footer",
|
| 211 |
+
border_style: Optional[StyleType] = None,
|
| 212 |
+
title_style: Optional[StyleType] = None,
|
| 213 |
+
caption_style: Optional[StyleType] = None,
|
| 214 |
+
title_justify: "JustifyMethod" = "center",
|
| 215 |
+
caption_justify: "JustifyMethod" = "center",
|
| 216 |
+
highlight: bool = False,
|
| 217 |
+
) -> None:
|
| 218 |
+
self.columns: List[Column] = []
|
| 219 |
+
self.rows: List[Row] = []
|
| 220 |
+
self.title = title
|
| 221 |
+
self.caption = caption
|
| 222 |
+
self.width = width
|
| 223 |
+
self.min_width = min_width
|
| 224 |
+
self.box = box
|
| 225 |
+
self.safe_box = safe_box
|
| 226 |
+
self._padding = Padding.unpack(padding)
|
| 227 |
+
self.pad_edge = pad_edge
|
| 228 |
+
self._expand = expand
|
| 229 |
+
self.show_header = show_header
|
| 230 |
+
self.show_footer = show_footer
|
| 231 |
+
self.show_edge = show_edge
|
| 232 |
+
self.show_lines = show_lines
|
| 233 |
+
self.leading = leading
|
| 234 |
+
self.collapse_padding = collapse_padding
|
| 235 |
+
self.style = style
|
| 236 |
+
self.header_style = header_style or ""
|
| 237 |
+
self.footer_style = footer_style or ""
|
| 238 |
+
self.border_style = border_style
|
| 239 |
+
self.title_style = title_style
|
| 240 |
+
self.caption_style = caption_style
|
| 241 |
+
self.title_justify: "JustifyMethod" = title_justify
|
| 242 |
+
self.caption_justify: "JustifyMethod" = caption_justify
|
| 243 |
+
self.highlight = highlight
|
| 244 |
+
self.row_styles: Sequence[StyleType] = list(row_styles or [])
|
| 245 |
+
append_column = self.columns.append
|
| 246 |
+
for header in headers:
|
| 247 |
+
if isinstance(header, str):
|
| 248 |
+
self.add_column(header=header)
|
| 249 |
+
else:
|
| 250 |
+
header._index = len(self.columns)
|
| 251 |
+
append_column(header)
|
| 252 |
+
|
| 253 |
+
@classmethod
|
| 254 |
+
def grid(
|
| 255 |
+
cls,
|
| 256 |
+
*headers: Union[Column, str],
|
| 257 |
+
padding: PaddingDimensions = 0,
|
| 258 |
+
collapse_padding: bool = True,
|
| 259 |
+
pad_edge: bool = False,
|
| 260 |
+
expand: bool = False,
|
| 261 |
+
) -> "Table":
|
| 262 |
+
"""Get a table with no lines, headers, or footer.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
*headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
|
| 266 |
+
padding (PaddingDimensions, optional): Get padding around cells. Defaults to 0.
|
| 267 |
+
collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to True.
|
| 268 |
+
pad_edge (bool, optional): Enable padding around edges of table. Defaults to False.
|
| 269 |
+
expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
|
| 270 |
+
|
| 271 |
+
Returns:
|
| 272 |
+
Table: A table instance.
|
| 273 |
+
"""
|
| 274 |
+
return cls(
|
| 275 |
+
*headers,
|
| 276 |
+
box=None,
|
| 277 |
+
padding=padding,
|
| 278 |
+
collapse_padding=collapse_padding,
|
| 279 |
+
show_header=False,
|
| 280 |
+
show_footer=False,
|
| 281 |
+
show_edge=False,
|
| 282 |
+
pad_edge=pad_edge,
|
| 283 |
+
expand=expand,
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
@property
|
| 287 |
+
def expand(self) -> bool:
|
| 288 |
+
"""Setting a non-None self.width implies expand."""
|
| 289 |
+
return self._expand or self.width is not None
|
| 290 |
+
|
| 291 |
+
@expand.setter
|
| 292 |
+
def expand(self, expand: bool) -> None:
|
| 293 |
+
"""Set expand."""
|
| 294 |
+
self._expand = expand
|
| 295 |
+
|
| 296 |
+
@property
|
| 297 |
+
def _extra_width(self) -> int:
|
| 298 |
+
"""Get extra width to add to cell content."""
|
| 299 |
+
width = 0
|
| 300 |
+
if self.box and self.show_edge:
|
| 301 |
+
width += 2
|
| 302 |
+
if self.box:
|
| 303 |
+
width += len(self.columns) - 1
|
| 304 |
+
return width
|
| 305 |
+
|
| 306 |
+
@property
|
| 307 |
+
def row_count(self) -> int:
|
| 308 |
+
"""Get the current number of rows."""
|
| 309 |
+
return len(self.rows)
|
| 310 |
+
|
| 311 |
+
def get_row_style(self, console: "Console", index: int) -> StyleType:
|
| 312 |
+
"""Get the current row style."""
|
| 313 |
+
style = Style.null()
|
| 314 |
+
if self.row_styles:
|
| 315 |
+
style += console.get_style(self.row_styles[index % len(self.row_styles)])
|
| 316 |
+
row_style = self.rows[index].style
|
| 317 |
+
if row_style is not None:
|
| 318 |
+
style += console.get_style(row_style)
|
| 319 |
+
return style
|
| 320 |
+
|
| 321 |
+
def __rich_measure__(
|
| 322 |
+
self, console: "Console", options: "ConsoleOptions"
|
| 323 |
+
) -> Measurement:
|
| 324 |
+
max_width = options.max_width
|
| 325 |
+
if self.width is not None:
|
| 326 |
+
max_width = self.width
|
| 327 |
+
if max_width < 0:
|
| 328 |
+
return Measurement(0, 0)
|
| 329 |
+
|
| 330 |
+
extra_width = self._extra_width
|
| 331 |
+
max_width = sum(
|
| 332 |
+
self._calculate_column_widths(
|
| 333 |
+
console, options.update_width(max_width - extra_width)
|
| 334 |
+
)
|
| 335 |
+
)
|
| 336 |
+
_measure_column = self._measure_column
|
| 337 |
+
|
| 338 |
+
measurements = [
|
| 339 |
+
_measure_column(console, options.update_width(max_width), column)
|
| 340 |
+
for column in self.columns
|
| 341 |
+
]
|
| 342 |
+
minimum_width = (
|
| 343 |
+
sum(measurement.minimum for measurement in measurements) + extra_width
|
| 344 |
+
)
|
| 345 |
+
maximum_width = (
|
| 346 |
+
sum(measurement.maximum for measurement in measurements) + extra_width
|
| 347 |
+
if (self.width is None)
|
| 348 |
+
else self.width
|
| 349 |
+
)
|
| 350 |
+
measurement = Measurement(minimum_width, maximum_width)
|
| 351 |
+
measurement = measurement.clamp(self.min_width)
|
| 352 |
+
return measurement
|
| 353 |
+
|
| 354 |
+
@property
|
| 355 |
+
def padding(self) -> Tuple[int, int, int, int]:
|
| 356 |
+
"""Get cell padding."""
|
| 357 |
+
return self._padding
|
| 358 |
+
|
| 359 |
+
@padding.setter
|
| 360 |
+
def padding(self, padding: PaddingDimensions) -> "Table":
|
| 361 |
+
"""Set cell padding."""
|
| 362 |
+
self._padding = Padding.unpack(padding)
|
| 363 |
+
return self
|
| 364 |
+
|
| 365 |
+
def add_column(
|
| 366 |
+
self,
|
| 367 |
+
header: "RenderableType" = "",
|
| 368 |
+
footer: "RenderableType" = "",
|
| 369 |
+
*,
|
| 370 |
+
header_style: Optional[StyleType] = None,
|
| 371 |
+
highlight: Optional[bool] = None,
|
| 372 |
+
footer_style: Optional[StyleType] = None,
|
| 373 |
+
style: Optional[StyleType] = None,
|
| 374 |
+
justify: "JustifyMethod" = "left",
|
| 375 |
+
vertical: "VerticalAlignMethod" = "top",
|
| 376 |
+
overflow: "OverflowMethod" = "ellipsis",
|
| 377 |
+
width: Optional[int] = None,
|
| 378 |
+
min_width: Optional[int] = None,
|
| 379 |
+
max_width: Optional[int] = None,
|
| 380 |
+
ratio: Optional[int] = None,
|
| 381 |
+
no_wrap: bool = False,
|
| 382 |
+
) -> None:
|
| 383 |
+
"""Add a column to the table.
|
| 384 |
+
|
| 385 |
+
Args:
|
| 386 |
+
header (RenderableType, optional): Text or renderable for the header.
|
| 387 |
+
Defaults to "".
|
| 388 |
+
footer (RenderableType, optional): Text or renderable for the footer.
|
| 389 |
+
Defaults to "".
|
| 390 |
+
header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None.
|
| 391 |
+
highlight (bool, optional): Whether to highlight the text. The default of None uses the value of the table (self) object.
|
| 392 |
+
footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None.
|
| 393 |
+
style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None.
|
| 394 |
+
justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".
|
| 395 |
+
vertical (VerticalAlignMethod, optional): Vertical alignment, one of "top", "middle", or "bottom". Defaults to "top".
|
| 396 |
+
overflow (OverflowMethod): Overflow method: "crop", "fold", "ellipsis". Defaults to "ellipsis".
|
| 397 |
+
width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None.
|
| 398 |
+
min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None.
|
| 399 |
+
max_width (Optional[int], optional): Maximum width of column, or ``None`` for no maximum. Defaults to None.
|
| 400 |
+
ratio (int, optional): Flexible ratio for the column (requires ``Table.expand`` or ``Table.width``). Defaults to None.
|
| 401 |
+
no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.
|
| 402 |
+
"""
|
| 403 |
+
|
| 404 |
+
column = Column(
|
| 405 |
+
_index=len(self.columns),
|
| 406 |
+
header=header,
|
| 407 |
+
footer=footer,
|
| 408 |
+
header_style=header_style or "",
|
| 409 |
+
highlight=highlight if highlight is not None else self.highlight,
|
| 410 |
+
footer_style=footer_style or "",
|
| 411 |
+
style=style or "",
|
| 412 |
+
justify=justify,
|
| 413 |
+
vertical=vertical,
|
| 414 |
+
overflow=overflow,
|
| 415 |
+
width=width,
|
| 416 |
+
min_width=min_width,
|
| 417 |
+
max_width=max_width,
|
| 418 |
+
ratio=ratio,
|
| 419 |
+
no_wrap=no_wrap,
|
| 420 |
+
)
|
| 421 |
+
self.columns.append(column)
|
| 422 |
+
|
| 423 |
+
def add_row(
|
| 424 |
+
self,
|
| 425 |
+
*renderables: Optional["RenderableType"],
|
| 426 |
+
style: Optional[StyleType] = None,
|
| 427 |
+
end_section: bool = False,
|
| 428 |
+
) -> None:
|
| 429 |
+
"""Add a row of renderables.
|
| 430 |
+
|
| 431 |
+
Args:
|
| 432 |
+
*renderables (None or renderable): Each cell in a row must be a renderable object (including str),
|
| 433 |
+
or ``None`` for a blank cell.
|
| 434 |
+
style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.
|
| 435 |
+
end_section (bool, optional): End a section and draw a line. Defaults to False.
|
| 436 |
+
|
| 437 |
+
Raises:
|
| 438 |
+
errors.NotRenderableError: If you add something that can't be rendered.
|
| 439 |
+
"""
|
| 440 |
+
|
| 441 |
+
def add_cell(column: Column, renderable: "RenderableType") -> None:
|
| 442 |
+
column._cells.append(renderable)
|
| 443 |
+
|
| 444 |
+
cell_renderables: List[Optional["RenderableType"]] = list(renderables)
|
| 445 |
+
|
| 446 |
+
columns = self.columns
|
| 447 |
+
if len(cell_renderables) < len(columns):
|
| 448 |
+
cell_renderables = [
|
| 449 |
+
*cell_renderables,
|
| 450 |
+
*[None] * (len(columns) - len(cell_renderables)),
|
| 451 |
+
]
|
| 452 |
+
for index, renderable in enumerate(cell_renderables):
|
| 453 |
+
if index == len(columns):
|
| 454 |
+
column = Column(_index=index, highlight=self.highlight)
|
| 455 |
+
for _ in self.rows:
|
| 456 |
+
add_cell(column, Text(""))
|
| 457 |
+
self.columns.append(column)
|
| 458 |
+
else:
|
| 459 |
+
column = columns[index]
|
| 460 |
+
if renderable is None:
|
| 461 |
+
add_cell(column, "")
|
| 462 |
+
elif is_renderable(renderable):
|
| 463 |
+
add_cell(column, renderable)
|
| 464 |
+
else:
|
| 465 |
+
raise errors.NotRenderableError(
|
| 466 |
+
f"unable to render {type(renderable).__name__}; a string or other renderable object is required"
|
| 467 |
+
)
|
| 468 |
+
self.rows.append(Row(style=style, end_section=end_section))
|
| 469 |
+
|
| 470 |
+
def add_section(self) -> None:
|
| 471 |
+
"""Add a new section (draw a line after current row)."""
|
| 472 |
+
|
| 473 |
+
if self.rows:
|
| 474 |
+
self.rows[-1].end_section = True
|
| 475 |
+
|
| 476 |
+
def __rich_console__(
|
| 477 |
+
self, console: "Console", options: "ConsoleOptions"
|
| 478 |
+
) -> "RenderResult":
|
| 479 |
+
if not self.columns:
|
| 480 |
+
yield Segment("\n")
|
| 481 |
+
return
|
| 482 |
+
|
| 483 |
+
max_width = options.max_width
|
| 484 |
+
if self.width is not None:
|
| 485 |
+
max_width = self.width
|
| 486 |
+
|
| 487 |
+
extra_width = self._extra_width
|
| 488 |
+
widths = self._calculate_column_widths(
|
| 489 |
+
console, options.update_width(max_width - extra_width)
|
| 490 |
+
)
|
| 491 |
+
table_width = sum(widths) + extra_width
|
| 492 |
+
|
| 493 |
+
render_options = options.update(
|
| 494 |
+
width=table_width, highlight=self.highlight, height=None
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
def render_annotation(
|
| 498 |
+
text: TextType, style: StyleType, justify: "JustifyMethod" = "center"
|
| 499 |
+
) -> "RenderResult":
|
| 500 |
+
render_text = (
|
| 501 |
+
console.render_str(text, style=style, highlight=False)
|
| 502 |
+
if isinstance(text, str)
|
| 503 |
+
else text
|
| 504 |
+
)
|
| 505 |
+
return console.render(
|
| 506 |
+
render_text, options=render_options.update(justify=justify)
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
if self.title:
|
| 510 |
+
yield from render_annotation(
|
| 511 |
+
self.title,
|
| 512 |
+
style=Style.pick_first(self.title_style, "table.title"),
|
| 513 |
+
justify=self.title_justify,
|
| 514 |
+
)
|
| 515 |
+
yield from self._render(console, render_options, widths)
|
| 516 |
+
if self.caption:
|
| 517 |
+
yield from render_annotation(
|
| 518 |
+
self.caption,
|
| 519 |
+
style=Style.pick_first(self.caption_style, "table.caption"),
|
| 520 |
+
justify=self.caption_justify,
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
def _calculate_column_widths(
|
| 524 |
+
self, console: "Console", options: "ConsoleOptions"
|
| 525 |
+
) -> List[int]:
|
| 526 |
+
"""Calculate the widths of each column, including padding, not including borders."""
|
| 527 |
+
max_width = options.max_width
|
| 528 |
+
columns = self.columns
|
| 529 |
+
width_ranges = [
|
| 530 |
+
self._measure_column(console, options, column) for column in columns
|
| 531 |
+
]
|
| 532 |
+
widths = [_range.maximum or 1 for _range in width_ranges]
|
| 533 |
+
get_padding_width = self._get_padding_width
|
| 534 |
+
extra_width = self._extra_width
|
| 535 |
+
if self.expand:
|
| 536 |
+
ratios = [col.ratio or 0 for col in columns if col.flexible]
|
| 537 |
+
if any(ratios):
|
| 538 |
+
fixed_widths = [
|
| 539 |
+
0 if column.flexible else _range.maximum
|
| 540 |
+
for _range, column in zip(width_ranges, columns)
|
| 541 |
+
]
|
| 542 |
+
flex_minimum = [
|
| 543 |
+
(column.width or 1) + get_padding_width(column._index)
|
| 544 |
+
for column in columns
|
| 545 |
+
if column.flexible
|
| 546 |
+
]
|
| 547 |
+
flexible_width = max_width - sum(fixed_widths)
|
| 548 |
+
flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum)
|
| 549 |
+
iter_flex_widths = iter(flex_widths)
|
| 550 |
+
for index, column in enumerate(columns):
|
| 551 |
+
if column.flexible:
|
| 552 |
+
widths[index] = fixed_widths[index] + next(iter_flex_widths)
|
| 553 |
+
table_width = sum(widths)
|
| 554 |
+
|
| 555 |
+
if table_width > max_width:
|
| 556 |
+
widths = self._collapse_widths(
|
| 557 |
+
widths,
|
| 558 |
+
[(column.width is None and not column.no_wrap) for column in columns],
|
| 559 |
+
max_width,
|
| 560 |
+
)
|
| 561 |
+
table_width = sum(widths)
|
| 562 |
+
# last resort, reduce columns evenly
|
| 563 |
+
if table_width > max_width:
|
| 564 |
+
excess_width = table_width - max_width
|
| 565 |
+
widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths)
|
| 566 |
+
table_width = sum(widths)
|
| 567 |
+
|
| 568 |
+
width_ranges = [
|
| 569 |
+
self._measure_column(console, options.update_width(width), column)
|
| 570 |
+
for width, column in zip(widths, columns)
|
| 571 |
+
]
|
| 572 |
+
widths = [_range.maximum or 0 for _range in width_ranges]
|
| 573 |
+
|
| 574 |
+
if (table_width < max_width and self.expand) or (
|
| 575 |
+
self.min_width is not None and table_width < (self.min_width - extra_width)
|
| 576 |
+
):
|
| 577 |
+
_max_width = (
|
| 578 |
+
max_width
|
| 579 |
+
if self.min_width is None
|
| 580 |
+
else min(self.min_width - extra_width, max_width)
|
| 581 |
+
)
|
| 582 |
+
pad_widths = ratio_distribute(_max_width - table_width, widths)
|
| 583 |
+
widths = [_width + pad for _width, pad in zip(widths, pad_widths)]
|
| 584 |
+
|
| 585 |
+
return widths
|
| 586 |
+
|
| 587 |
+
@classmethod
|
| 588 |
+
def _collapse_widths(
|
| 589 |
+
cls, widths: List[int], wrapable: List[bool], max_width: int
|
| 590 |
+
) -> List[int]:
|
| 591 |
+
"""Reduce widths so that the total is under max_width.
|
| 592 |
+
|
| 593 |
+
Args:
|
| 594 |
+
widths (List[int]): List of widths.
|
| 595 |
+
wrapable (List[bool]): List of booleans that indicate if a column may shrink.
|
| 596 |
+
max_width (int): Maximum width to reduce to.
|
| 597 |
+
|
| 598 |
+
Returns:
|
| 599 |
+
List[int]: A new list of widths.
|
| 600 |
+
"""
|
| 601 |
+
total_width = sum(widths)
|
| 602 |
+
excess_width = total_width - max_width
|
| 603 |
+
if any(wrapable):
|
| 604 |
+
while total_width and excess_width > 0:
|
| 605 |
+
max_column = max(
|
| 606 |
+
width for width, allow_wrap in zip(widths, wrapable) if allow_wrap
|
| 607 |
+
)
|
| 608 |
+
second_max_column = max(
|
| 609 |
+
width if allow_wrap and width != max_column else 0
|
| 610 |
+
for width, allow_wrap in zip(widths, wrapable)
|
| 611 |
+
)
|
| 612 |
+
column_difference = max_column - second_max_column
|
| 613 |
+
ratios = [
|
| 614 |
+
(1 if (width == max_column and allow_wrap) else 0)
|
| 615 |
+
for width, allow_wrap in zip(widths, wrapable)
|
| 616 |
+
]
|
| 617 |
+
if not any(ratios) or not column_difference:
|
| 618 |
+
break
|
| 619 |
+
max_reduce = [min(excess_width, column_difference)] * len(widths)
|
| 620 |
+
widths = ratio_reduce(excess_width, ratios, max_reduce, widths)
|
| 621 |
+
|
| 622 |
+
total_width = sum(widths)
|
| 623 |
+
excess_width = total_width - max_width
|
| 624 |
+
return widths
|
| 625 |
+
|
| 626 |
+
def _get_cells(
|
| 627 |
+
self, console: "Console", column_index: int, column: Column
|
| 628 |
+
) -> Iterable[_Cell]:
|
| 629 |
+
"""Get all the cells with padding and optional header."""
|
| 630 |
+
|
| 631 |
+
collapse_padding = self.collapse_padding
|
| 632 |
+
pad_edge = self.pad_edge
|
| 633 |
+
padding = self.padding
|
| 634 |
+
any_padding = any(padding)
|
| 635 |
+
|
| 636 |
+
first_column = column_index == 0
|
| 637 |
+
last_column = column_index == len(self.columns) - 1
|
| 638 |
+
|
| 639 |
+
_padding_cache: Dict[Tuple[bool, bool], Tuple[int, int, int, int]] = {}
|
| 640 |
+
|
| 641 |
+
def get_padding(first_row: bool, last_row: bool) -> Tuple[int, int, int, int]:
|
| 642 |
+
cached = _padding_cache.get((first_row, last_row))
|
| 643 |
+
if cached:
|
| 644 |
+
return cached
|
| 645 |
+
top, right, bottom, left = padding
|
| 646 |
+
|
| 647 |
+
if collapse_padding:
|
| 648 |
+
if not first_column:
|
| 649 |
+
left = max(0, left - right)
|
| 650 |
+
if not last_row:
|
| 651 |
+
bottom = max(0, top - bottom)
|
| 652 |
+
|
| 653 |
+
if not pad_edge:
|
| 654 |
+
if first_column:
|
| 655 |
+
left = 0
|
| 656 |
+
if last_column:
|
| 657 |
+
right = 0
|
| 658 |
+
if first_row:
|
| 659 |
+
top = 0
|
| 660 |
+
if last_row:
|
| 661 |
+
bottom = 0
|
| 662 |
+
_padding = (top, right, bottom, left)
|
| 663 |
+
_padding_cache[(first_row, last_row)] = _padding
|
| 664 |
+
return _padding
|
| 665 |
+
|
| 666 |
+
raw_cells: List[Tuple[StyleType, "RenderableType"]] = []
|
| 667 |
+
_append = raw_cells.append
|
| 668 |
+
get_style = console.get_style
|
| 669 |
+
if self.show_header:
|
| 670 |
+
header_style = get_style(self.header_style or "") + get_style(
|
| 671 |
+
column.header_style
|
| 672 |
+
)
|
| 673 |
+
_append((header_style, column.header))
|
| 674 |
+
cell_style = get_style(column.style or "")
|
| 675 |
+
for cell in column.cells:
|
| 676 |
+
_append((cell_style, cell))
|
| 677 |
+
if self.show_footer:
|
| 678 |
+
footer_style = get_style(self.footer_style or "") + get_style(
|
| 679 |
+
column.footer_style
|
| 680 |
+
)
|
| 681 |
+
_append((footer_style, column.footer))
|
| 682 |
+
|
| 683 |
+
if any_padding:
|
| 684 |
+
_Padding = Padding
|
| 685 |
+
for first, last, (style, renderable) in loop_first_last(raw_cells):
|
| 686 |
+
yield _Cell(
|
| 687 |
+
style,
|
| 688 |
+
_Padding(renderable, get_padding(first, last)),
|
| 689 |
+
getattr(renderable, "vertical", None) or column.vertical,
|
| 690 |
+
)
|
| 691 |
+
else:
|
| 692 |
+
for style, renderable in raw_cells:
|
| 693 |
+
yield _Cell(
|
| 694 |
+
style,
|
| 695 |
+
renderable,
|
| 696 |
+
getattr(renderable, "vertical", None) or column.vertical,
|
| 697 |
+
)
|
| 698 |
+
|
| 699 |
+
def _get_padding_width(self, column_index: int) -> int:
|
| 700 |
+
"""Get extra width from padding."""
|
| 701 |
+
_, pad_right, _, pad_left = self.padding
|
| 702 |
+
if self.collapse_padding:
|
| 703 |
+
if column_index > 0:
|
| 704 |
+
pad_left = max(0, pad_left - pad_right)
|
| 705 |
+
return pad_left + pad_right
|
| 706 |
+
|
| 707 |
+
def _measure_column(
|
| 708 |
+
self,
|
| 709 |
+
console: "Console",
|
| 710 |
+
options: "ConsoleOptions",
|
| 711 |
+
column: Column,
|
| 712 |
+
) -> Measurement:
|
| 713 |
+
"""Get the minimum and maximum width of the column."""
|
| 714 |
+
|
| 715 |
+
max_width = options.max_width
|
| 716 |
+
if max_width < 1:
|
| 717 |
+
return Measurement(0, 0)
|
| 718 |
+
|
| 719 |
+
padding_width = self._get_padding_width(column._index)
|
| 720 |
+
|
| 721 |
+
if column.width is not None:
|
| 722 |
+
# Fixed width column
|
| 723 |
+
return Measurement(
|
| 724 |
+
column.width + padding_width, column.width + padding_width
|
| 725 |
+
).with_maximum(max_width)
|
| 726 |
+
# Flexible column, we need to measure contents
|
| 727 |
+
min_widths: List[int] = []
|
| 728 |
+
max_widths: List[int] = []
|
| 729 |
+
append_min = min_widths.append
|
| 730 |
+
append_max = max_widths.append
|
| 731 |
+
get_render_width = Measurement.get
|
| 732 |
+
for cell in self._get_cells(console, column._index, column):
|
| 733 |
+
_min, _max = get_render_width(console, options, cell.renderable)
|
| 734 |
+
append_min(_min)
|
| 735 |
+
append_max(_max)
|
| 736 |
+
|
| 737 |
+
measurement = Measurement(
|
| 738 |
+
max(min_widths) if min_widths else 1,
|
| 739 |
+
max(max_widths) if max_widths else max_width,
|
| 740 |
+
).with_maximum(max_width)
|
| 741 |
+
measurement = measurement.clamp(
|
| 742 |
+
None if column.min_width is None else column.min_width + padding_width,
|
| 743 |
+
None if column.max_width is None else column.max_width + padding_width,
|
| 744 |
+
)
|
| 745 |
+
return measurement
|
| 746 |
+
|
| 747 |
+
def _render(
|
| 748 |
+
self, console: "Console", options: "ConsoleOptions", widths: List[int]
|
| 749 |
+
) -> "RenderResult":
|
| 750 |
+
table_style = console.get_style(self.style or "")
|
| 751 |
+
|
| 752 |
+
border_style = table_style + console.get_style(self.border_style or "")
|
| 753 |
+
_column_cells = (
|
| 754 |
+
self._get_cells(console, column_index, column)
|
| 755 |
+
for column_index, column in enumerate(self.columns)
|
| 756 |
+
)
|
| 757 |
+
row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
|
| 758 |
+
_box = (
|
| 759 |
+
self.box.substitute(
|
| 760 |
+
options, safe=pick_bool(self.safe_box, console.safe_box)
|
| 761 |
+
)
|
| 762 |
+
if self.box
|
| 763 |
+
else None
|
| 764 |
+
)
|
| 765 |
+
_box = _box.get_plain_headed_box() if _box and not self.show_header else _box
|
| 766 |
+
|
| 767 |
+
new_line = Segment.line()
|
| 768 |
+
|
| 769 |
+
columns = self.columns
|
| 770 |
+
show_header = self.show_header
|
| 771 |
+
show_footer = self.show_footer
|
| 772 |
+
show_edge = self.show_edge
|
| 773 |
+
show_lines = self.show_lines
|
| 774 |
+
leading = self.leading
|
| 775 |
+
|
| 776 |
+
_Segment = Segment
|
| 777 |
+
if _box:
|
| 778 |
+
box_segments = [
|
| 779 |
+
(
|
| 780 |
+
_Segment(_box.head_left, border_style),
|
| 781 |
+
_Segment(_box.head_right, border_style),
|
| 782 |
+
_Segment(_box.head_vertical, border_style),
|
| 783 |
+
),
|
| 784 |
+
(
|
| 785 |
+
_Segment(_box.mid_left, border_style),
|
| 786 |
+
_Segment(_box.mid_right, border_style),
|
| 787 |
+
_Segment(_box.mid_vertical, border_style),
|
| 788 |
+
),
|
| 789 |
+
(
|
| 790 |
+
_Segment(_box.foot_left, border_style),
|
| 791 |
+
_Segment(_box.foot_right, border_style),
|
| 792 |
+
_Segment(_box.foot_vertical, border_style),
|
| 793 |
+
),
|
| 794 |
+
]
|
| 795 |
+
if show_edge:
|
| 796 |
+
yield _Segment(_box.get_top(widths), border_style)
|
| 797 |
+
yield new_line
|
| 798 |
+
else:
|
| 799 |
+
box_segments = []
|
| 800 |
+
|
| 801 |
+
get_row_style = self.get_row_style
|
| 802 |
+
get_style = console.get_style
|
| 803 |
+
|
| 804 |
+
for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):
|
| 805 |
+
header_row = first and show_header
|
| 806 |
+
footer_row = last and show_footer
|
| 807 |
+
row = (
|
| 808 |
+
self.rows[index - show_header]
|
| 809 |
+
if (not header_row and not footer_row)
|
| 810 |
+
else None
|
| 811 |
+
)
|
| 812 |
+
max_height = 1
|
| 813 |
+
cells: List[List[List[Segment]]] = []
|
| 814 |
+
if header_row or footer_row:
|
| 815 |
+
row_style = Style.null()
|
| 816 |
+
else:
|
| 817 |
+
row_style = get_style(
|
| 818 |
+
get_row_style(console, index - 1 if show_header else index)
|
| 819 |
+
)
|
| 820 |
+
for width, cell, column in zip(widths, row_cell, columns):
|
| 821 |
+
render_options = options.update(
|
| 822 |
+
width=width,
|
| 823 |
+
justify=column.justify,
|
| 824 |
+
no_wrap=column.no_wrap,
|
| 825 |
+
overflow=column.overflow,
|
| 826 |
+
height=None,
|
| 827 |
+
highlight=column.highlight,
|
| 828 |
+
)
|
| 829 |
+
lines = console.render_lines(
|
| 830 |
+
cell.renderable,
|
| 831 |
+
render_options,
|
| 832 |
+
style=get_style(cell.style) + row_style,
|
| 833 |
+
)
|
| 834 |
+
max_height = max(max_height, len(lines))
|
| 835 |
+
cells.append(lines)
|
| 836 |
+
|
| 837 |
+
row_height = max(len(cell) for cell in cells)
|
| 838 |
+
|
| 839 |
+
def align_cell(
|
| 840 |
+
cell: List[List[Segment]],
|
| 841 |
+
vertical: "VerticalAlignMethod",
|
| 842 |
+
width: int,
|
| 843 |
+
style: Style,
|
| 844 |
+
) -> List[List[Segment]]:
|
| 845 |
+
if header_row:
|
| 846 |
+
vertical = "bottom"
|
| 847 |
+
elif footer_row:
|
| 848 |
+
vertical = "top"
|
| 849 |
+
|
| 850 |
+
if vertical == "top":
|
| 851 |
+
return _Segment.align_top(cell, width, row_height, style)
|
| 852 |
+
elif vertical == "middle":
|
| 853 |
+
return _Segment.align_middle(cell, width, row_height, style)
|
| 854 |
+
return _Segment.align_bottom(cell, width, row_height, style)
|
| 855 |
+
|
| 856 |
+
cells[:] = [
|
| 857 |
+
_Segment.set_shape(
|
| 858 |
+
align_cell(
|
| 859 |
+
cell,
|
| 860 |
+
_cell.vertical,
|
| 861 |
+
width,
|
| 862 |
+
get_style(_cell.style) + row_style,
|
| 863 |
+
),
|
| 864 |
+
width,
|
| 865 |
+
max_height,
|
| 866 |
+
)
|
| 867 |
+
for width, _cell, cell, column in zip(widths, row_cell, cells, columns)
|
| 868 |
+
]
|
| 869 |
+
|
| 870 |
+
if _box:
|
| 871 |
+
if last and show_footer:
|
| 872 |
+
yield _Segment(
|
| 873 |
+
_box.get_row(widths, "foot", edge=show_edge), border_style
|
| 874 |
+
)
|
| 875 |
+
yield new_line
|
| 876 |
+
left, right, _divider = box_segments[0 if first else (2 if last else 1)]
|
| 877 |
+
|
| 878 |
+
# If the column divider is whitespace also style it with the row background
|
| 879 |
+
divider = (
|
| 880 |
+
_divider
|
| 881 |
+
if _divider.text.strip()
|
| 882 |
+
else _Segment(
|
| 883 |
+
_divider.text, row_style.background_style + _divider.style
|
| 884 |
+
)
|
| 885 |
+
)
|
| 886 |
+
for line_no in range(max_height):
|
| 887 |
+
if show_edge:
|
| 888 |
+
yield left
|
| 889 |
+
for last_cell, rendered_cell in loop_last(cells):
|
| 890 |
+
yield from rendered_cell[line_no]
|
| 891 |
+
if not last_cell:
|
| 892 |
+
yield divider
|
| 893 |
+
if show_edge:
|
| 894 |
+
yield right
|
| 895 |
+
yield new_line
|
| 896 |
+
else:
|
| 897 |
+
for line_no in range(max_height):
|
| 898 |
+
for rendered_cell in cells:
|
| 899 |
+
yield from rendered_cell[line_no]
|
| 900 |
+
yield new_line
|
| 901 |
+
if _box and first and show_header:
|
| 902 |
+
yield _Segment(
|
| 903 |
+
_box.get_row(widths, "head", edge=show_edge), border_style
|
| 904 |
+
)
|
| 905 |
+
yield new_line
|
| 906 |
+
end_section = row and row.end_section
|
| 907 |
+
if _box and (show_lines or leading or end_section):
|
| 908 |
+
if (
|
| 909 |
+
not last
|
| 910 |
+
and not (show_footer and index >= len(row_cells) - 2)
|
| 911 |
+
and not (show_header and header_row)
|
| 912 |
+
):
|
| 913 |
+
if leading:
|
| 914 |
+
yield _Segment(
|
| 915 |
+
_box.get_row(widths, "mid", edge=show_edge) * leading,
|
| 916 |
+
border_style,
|
| 917 |
+
)
|
| 918 |
+
else:
|
| 919 |
+
yield _Segment(
|
| 920 |
+
_box.get_row(widths, "row", edge=show_edge), border_style
|
| 921 |
+
)
|
| 922 |
+
yield new_line
|
| 923 |
+
|
| 924 |
+
if _box and show_edge:
|
| 925 |
+
yield _Segment(_box.get_bottom(widths), border_style)
|
| 926 |
+
yield new_line
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
if __name__ == "__main__": # pragma: no cover
|
| 930 |
+
from pip._vendor.rich.console import Console
|
| 931 |
+
from pip._vendor.rich.highlighter import ReprHighlighter
|
| 932 |
+
from pip._vendor.rich.table import Table as Table
|
| 933 |
+
|
| 934 |
+
from ._timer import timer
|
| 935 |
+
|
| 936 |
+
with timer("Table render"):
|
| 937 |
+
table = Table(
|
| 938 |
+
title="Star Wars Movies",
|
| 939 |
+
caption="Rich example table",
|
| 940 |
+
caption_justify="right",
|
| 941 |
+
)
|
| 942 |
+
|
| 943 |
+
table.add_column(
|
| 944 |
+
"Released", header_style="bright_cyan", style="cyan", no_wrap=True
|
| 945 |
+
)
|
| 946 |
+
table.add_column("Title", style="magenta")
|
| 947 |
+
table.add_column("Box Office", justify="right", style="green")
|
| 948 |
+
|
| 949 |
+
table.add_row(
|
| 950 |
+
"Dec 20, 2019",
|
| 951 |
+
"Star Wars: The Rise of Skywalker",
|
| 952 |
+
"$952,110,690",
|
| 953 |
+
)
|
| 954 |
+
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
|
| 955 |
+
table.add_row(
|
| 956 |
+
"Dec 15, 2017",
|
| 957 |
+
"Star Wars Ep. V111: The Last Jedi",
|
| 958 |
+
"$1,332,539,889",
|
| 959 |
+
style="on black",
|
| 960 |
+
end_section=True,
|
| 961 |
+
)
|
| 962 |
+
table.add_row(
|
| 963 |
+
"Dec 16, 2016",
|
| 964 |
+
"Rogue One: A Star Wars Story",
|
| 965 |
+
"$1,332,439,889",
|
| 966 |
+
)
|
| 967 |
+
|
| 968 |
+
def header(text: str) -> None:
|
| 969 |
+
console.print()
|
| 970 |
+
console.rule(highlight(text))
|
| 971 |
+
console.print()
|
| 972 |
+
|
| 973 |
+
console = Console()
|
| 974 |
+
highlight = ReprHighlighter()
|
| 975 |
+
header("Example Table")
|
| 976 |
+
console.print(table, justify="center")
|
| 977 |
+
|
| 978 |
+
table.expand = True
|
| 979 |
+
header("expand=True")
|
| 980 |
+
console.print(table)
|
| 981 |
+
|
| 982 |
+
table.width = 50
|
| 983 |
+
header("width=50")
|
| 984 |
+
|
| 985 |
+
console.print(table, justify="center")
|
| 986 |
+
|
| 987 |
+
table.width = None
|
| 988 |
+
table.expand = False
|
| 989 |
+
table.row_styles = ["dim", "none"]
|
| 990 |
+
header("row_styles=['dim', 'none']")
|
| 991 |
+
|
| 992 |
+
console.print(table, justify="center")
|
| 993 |
+
|
| 994 |
+
table.width = None
|
| 995 |
+
table.expand = False
|
| 996 |
+
table.row_styles = ["dim", "none"]
|
| 997 |
+
table.leading = 1
|
| 998 |
+
header("leading=1, row_styles=['dim', 'none']")
|
| 999 |
+
console.print(table, justify="center")
|
| 1000 |
+
|
| 1001 |
+
table.width = None
|
| 1002 |
+
table.expand = False
|
| 1003 |
+
table.row_styles = ["dim", "none"]
|
| 1004 |
+
table.show_lines = True
|
| 1005 |
+
table.leading = 0
|
| 1006 |
+
header("show_lines=True, row_styles=['dim', 'none']")
|
| 1007 |
+
console.print(table, justify="center")
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more
|
| 3 |
+
"""
|
| 4 |
+
from __future__ import absolute_import
|
| 5 |
+
|
| 6 |
+
# Set default logging handler to avoid "No handler found" warnings.
|
| 7 |
+
import logging
|
| 8 |
+
import warnings
|
| 9 |
+
from logging import NullHandler
|
| 10 |
+
|
| 11 |
+
from . import exceptions
|
| 12 |
+
from ._version import __version__
|
| 13 |
+
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url
|
| 14 |
+
from .filepost import encode_multipart_formdata
|
| 15 |
+
from .poolmanager import PoolManager, ProxyManager, proxy_from_url
|
| 16 |
+
from .response import HTTPResponse
|
| 17 |
+
from .util.request import make_headers
|
| 18 |
+
from .util.retry import Retry
|
| 19 |
+
from .util.timeout import Timeout
|
| 20 |
+
from .util.url import get_host
|
| 21 |
+
|
| 22 |
+
# === NOTE TO REPACKAGERS AND VENDORS ===
|
| 23 |
+
# Please delete this block, this logic is only
|
| 24 |
+
# for urllib3 being distributed via PyPI.
|
| 25 |
+
# See: https://github.com/urllib3/urllib3/issues/2680
|
| 26 |
+
try:
|
| 27 |
+
import urllib3_secure_extra # type: ignore # noqa: F401
|
| 28 |
+
except ImportError:
|
| 29 |
+
pass
|
| 30 |
+
else:
|
| 31 |
+
warnings.warn(
|
| 32 |
+
"'urllib3[secure]' extra is deprecated and will be removed "
|
| 33 |
+
"in a future release of urllib3 2.x. Read more in this issue: "
|
| 34 |
+
"https://github.com/urllib3/urllib3/issues/2680",
|
| 35 |
+
category=DeprecationWarning,
|
| 36 |
+
stacklevel=2,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
__author__ = "Andrey Petrov (andrey.petrov@shazow.net)"
|
| 40 |
+
__license__ = "MIT"
|
| 41 |
+
__version__ = __version__
|
| 42 |
+
|
| 43 |
+
__all__ = (
|
| 44 |
+
"HTTPConnectionPool",
|
| 45 |
+
"HTTPSConnectionPool",
|
| 46 |
+
"PoolManager",
|
| 47 |
+
"ProxyManager",
|
| 48 |
+
"HTTPResponse",
|
| 49 |
+
"Retry",
|
| 50 |
+
"Timeout",
|
| 51 |
+
"add_stderr_logger",
|
| 52 |
+
"connection_from_url",
|
| 53 |
+
"disable_warnings",
|
| 54 |
+
"encode_multipart_formdata",
|
| 55 |
+
"get_host",
|
| 56 |
+
"make_headers",
|
| 57 |
+
"proxy_from_url",
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
logging.getLogger(__name__).addHandler(NullHandler())
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def add_stderr_logger(level=logging.DEBUG):
|
| 64 |
+
"""
|
| 65 |
+
Helper for quickly adding a StreamHandler to the logger. Useful for
|
| 66 |
+
debugging.
|
| 67 |
+
|
| 68 |
+
Returns the handler after adding it.
|
| 69 |
+
"""
|
| 70 |
+
# This method needs to be in this __init__.py to get the __name__ correct
|
| 71 |
+
# even if urllib3 is vendored within another package.
|
| 72 |
+
logger = logging.getLogger(__name__)
|
| 73 |
+
handler = logging.StreamHandler()
|
| 74 |
+
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
|
| 75 |
+
logger.addHandler(handler)
|
| 76 |
+
logger.setLevel(level)
|
| 77 |
+
logger.debug("Added a stderr logging handler to logger: %s", __name__)
|
| 78 |
+
return handler
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ... Clean up.
|
| 82 |
+
del NullHandler
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# All warning filters *must* be appended unless you're really certain that they
|
| 86 |
+
# shouldn't be: otherwise, it's very hard for users to use most Python
|
| 87 |
+
# mechanisms to silence them.
|
| 88 |
+
# SecurityWarning's always go off by default.
|
| 89 |
+
warnings.simplefilter("always", exceptions.SecurityWarning, append=True)
|
| 90 |
+
# SubjectAltNameWarning's should go off once per host
|
| 91 |
+
warnings.simplefilter("default", exceptions.SubjectAltNameWarning, append=True)
|
| 92 |
+
# InsecurePlatformWarning's don't vary between requests, so we keep it default.
|
| 93 |
+
warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True)
|
| 94 |
+
# SNIMissingWarnings should go off only once.
|
| 95 |
+
warnings.simplefilter("default", exceptions.SNIMissingWarning, append=True)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def disable_warnings(category=exceptions.HTTPWarning):
|
| 99 |
+
"""
|
| 100 |
+
Helper for quickly disabling all urllib3 warnings.
|
| 101 |
+
"""
|
| 102 |
+
warnings.simplefilter("ignore", category)
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (3.41 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_collections.cpython-312.pyc
ADDED
|
Binary file (16.4 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/_version.cpython-312.pyc
ADDED
|
Binary file (226 Bytes). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connection.cpython-312.pyc
ADDED
|
Binary file (20.4 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/connectionpool.cpython-312.pyc
ADDED
|
Binary file (36.5 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/exceptions.cpython-312.pyc
ADDED
|
Binary file (13.5 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/fields.cpython-312.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/filepost.cpython-312.pyc
ADDED
|
Binary file (4.02 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/poolmanager.cpython-312.pyc
ADDED
|
Binary file (20.4 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/request.cpython-312.pyc
ADDED
|
Binary file (7.3 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/__pycache__/response.cpython-312.pyc
ADDED
|
Binary file (34 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/_collections.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import absolute_import
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
from collections.abc import Mapping, MutableMapping
|
| 5 |
+
except ImportError:
|
| 6 |
+
from collections import Mapping, MutableMapping
|
| 7 |
+
try:
|
| 8 |
+
from threading import RLock
|
| 9 |
+
except ImportError: # Platform-specific: No threads available
|
| 10 |
+
|
| 11 |
+
class RLock:
|
| 12 |
+
def __enter__(self):
|
| 13 |
+
pass
|
| 14 |
+
|
| 15 |
+
def __exit__(self, exc_type, exc_value, traceback):
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
from collections import OrderedDict
|
| 20 |
+
|
| 21 |
+
from .exceptions import InvalidHeader
|
| 22 |
+
from .packages import six
|
| 23 |
+
from .packages.six import iterkeys, itervalues
|
| 24 |
+
|
| 25 |
+
__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
_Null = object()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class RecentlyUsedContainer(MutableMapping):
|
| 32 |
+
"""
|
| 33 |
+
Provides a thread-safe dict-like container which maintains up to
|
| 34 |
+
``maxsize`` keys while throwing away the least-recently-used keys beyond
|
| 35 |
+
``maxsize``.
|
| 36 |
+
|
| 37 |
+
:param maxsize:
|
| 38 |
+
Maximum number of recent elements to retain.
|
| 39 |
+
|
| 40 |
+
:param dispose_func:
|
| 41 |
+
Every time an item is evicted from the container,
|
| 42 |
+
``dispose_func(value)`` is called. Callback which will get called
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
ContainerCls = OrderedDict
|
| 46 |
+
|
| 47 |
+
def __init__(self, maxsize=10, dispose_func=None):
|
| 48 |
+
self._maxsize = maxsize
|
| 49 |
+
self.dispose_func = dispose_func
|
| 50 |
+
|
| 51 |
+
self._container = self.ContainerCls()
|
| 52 |
+
self.lock = RLock()
|
| 53 |
+
|
| 54 |
+
def __getitem__(self, key):
|
| 55 |
+
# Re-insert the item, moving it to the end of the eviction line.
|
| 56 |
+
with self.lock:
|
| 57 |
+
item = self._container.pop(key)
|
| 58 |
+
self._container[key] = item
|
| 59 |
+
return item
|
| 60 |
+
|
| 61 |
+
def __setitem__(self, key, value):
|
| 62 |
+
evicted_value = _Null
|
| 63 |
+
with self.lock:
|
| 64 |
+
# Possibly evict the existing value of 'key'
|
| 65 |
+
evicted_value = self._container.get(key, _Null)
|
| 66 |
+
self._container[key] = value
|
| 67 |
+
|
| 68 |
+
# If we didn't evict an existing value, we might have to evict the
|
| 69 |
+
# least recently used item from the beginning of the container.
|
| 70 |
+
if len(self._container) > self._maxsize:
|
| 71 |
+
_key, evicted_value = self._container.popitem(last=False)
|
| 72 |
+
|
| 73 |
+
if self.dispose_func and evicted_value is not _Null:
|
| 74 |
+
self.dispose_func(evicted_value)
|
| 75 |
+
|
| 76 |
+
def __delitem__(self, key):
|
| 77 |
+
with self.lock:
|
| 78 |
+
value = self._container.pop(key)
|
| 79 |
+
|
| 80 |
+
if self.dispose_func:
|
| 81 |
+
self.dispose_func(value)
|
| 82 |
+
|
| 83 |
+
def __len__(self):
|
| 84 |
+
with self.lock:
|
| 85 |
+
return len(self._container)
|
| 86 |
+
|
| 87 |
+
def __iter__(self):
|
| 88 |
+
raise NotImplementedError(
|
| 89 |
+
"Iteration over this class is unlikely to be threadsafe."
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
def clear(self):
|
| 93 |
+
with self.lock:
|
| 94 |
+
# Copy pointers to all values, then wipe the mapping
|
| 95 |
+
values = list(itervalues(self._container))
|
| 96 |
+
self._container.clear()
|
| 97 |
+
|
| 98 |
+
if self.dispose_func:
|
| 99 |
+
for value in values:
|
| 100 |
+
self.dispose_func(value)
|
| 101 |
+
|
| 102 |
+
def keys(self):
|
| 103 |
+
with self.lock:
|
| 104 |
+
return list(iterkeys(self._container))
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class HTTPHeaderDict(MutableMapping):
|
| 108 |
+
"""
|
| 109 |
+
:param headers:
|
| 110 |
+
An iterable of field-value pairs. Must not contain multiple field names
|
| 111 |
+
when compared case-insensitively.
|
| 112 |
+
|
| 113 |
+
:param kwargs:
|
| 114 |
+
Additional field-value pairs to pass in to ``dict.update``.
|
| 115 |
+
|
| 116 |
+
A ``dict`` like container for storing HTTP Headers.
|
| 117 |
+
|
| 118 |
+
Field names are stored and compared case-insensitively in compliance with
|
| 119 |
+
RFC 7230. Iteration provides the first case-sensitive key seen for each
|
| 120 |
+
case-insensitive pair.
|
| 121 |
+
|
| 122 |
+
Using ``__setitem__`` syntax overwrites fields that compare equal
|
| 123 |
+
case-insensitively in order to maintain ``dict``'s api. For fields that
|
| 124 |
+
compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
|
| 125 |
+
in a loop.
|
| 126 |
+
|
| 127 |
+
If multiple fields that are equal case-insensitively are passed to the
|
| 128 |
+
constructor or ``.update``, the behavior is undefined and some will be
|
| 129 |
+
lost.
|
| 130 |
+
|
| 131 |
+
>>> headers = HTTPHeaderDict()
|
| 132 |
+
>>> headers.add('Set-Cookie', 'foo=bar')
|
| 133 |
+
>>> headers.add('set-cookie', 'baz=quxx')
|
| 134 |
+
>>> headers['content-length'] = '7'
|
| 135 |
+
>>> headers['SET-cookie']
|
| 136 |
+
'foo=bar, baz=quxx'
|
| 137 |
+
>>> headers['Content-Length']
|
| 138 |
+
'7'
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
def __init__(self, headers=None, **kwargs):
|
| 142 |
+
super(HTTPHeaderDict, self).__init__()
|
| 143 |
+
self._container = OrderedDict()
|
| 144 |
+
if headers is not None:
|
| 145 |
+
if isinstance(headers, HTTPHeaderDict):
|
| 146 |
+
self._copy_from(headers)
|
| 147 |
+
else:
|
| 148 |
+
self.extend(headers)
|
| 149 |
+
if kwargs:
|
| 150 |
+
self.extend(kwargs)
|
| 151 |
+
|
| 152 |
+
def __setitem__(self, key, val):
|
| 153 |
+
self._container[key.lower()] = [key, val]
|
| 154 |
+
return self._container[key.lower()]
|
| 155 |
+
|
| 156 |
+
def __getitem__(self, key):
|
| 157 |
+
val = self._container[key.lower()]
|
| 158 |
+
return ", ".join(val[1:])
|
| 159 |
+
|
| 160 |
+
def __delitem__(self, key):
|
| 161 |
+
del self._container[key.lower()]
|
| 162 |
+
|
| 163 |
+
def __contains__(self, key):
|
| 164 |
+
return key.lower() in self._container
|
| 165 |
+
|
| 166 |
+
def __eq__(self, other):
|
| 167 |
+
if not isinstance(other, Mapping) and not hasattr(other, "keys"):
|
| 168 |
+
return False
|
| 169 |
+
if not isinstance(other, type(self)):
|
| 170 |
+
other = type(self)(other)
|
| 171 |
+
return dict((k.lower(), v) for k, v in self.itermerged()) == dict(
|
| 172 |
+
(k.lower(), v) for k, v in other.itermerged()
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
def __ne__(self, other):
|
| 176 |
+
return not self.__eq__(other)
|
| 177 |
+
|
| 178 |
+
if six.PY2: # Python 2
|
| 179 |
+
iterkeys = MutableMapping.iterkeys
|
| 180 |
+
itervalues = MutableMapping.itervalues
|
| 181 |
+
|
| 182 |
+
__marker = object()
|
| 183 |
+
|
| 184 |
+
def __len__(self):
|
| 185 |
+
return len(self._container)
|
| 186 |
+
|
| 187 |
+
def __iter__(self):
|
| 188 |
+
# Only provide the originally cased names
|
| 189 |
+
for vals in self._container.values():
|
| 190 |
+
yield vals[0]
|
| 191 |
+
|
| 192 |
+
def pop(self, key, default=__marker):
|
| 193 |
+
"""D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
| 194 |
+
If key is not found, d is returned if given, otherwise KeyError is raised.
|
| 195 |
+
"""
|
| 196 |
+
# Using the MutableMapping function directly fails due to the private marker.
|
| 197 |
+
# Using ordinary dict.pop would expose the internal structures.
|
| 198 |
+
# So let's reinvent the wheel.
|
| 199 |
+
try:
|
| 200 |
+
value = self[key]
|
| 201 |
+
except KeyError:
|
| 202 |
+
if default is self.__marker:
|
| 203 |
+
raise
|
| 204 |
+
return default
|
| 205 |
+
else:
|
| 206 |
+
del self[key]
|
| 207 |
+
return value
|
| 208 |
+
|
| 209 |
+
def discard(self, key):
|
| 210 |
+
try:
|
| 211 |
+
del self[key]
|
| 212 |
+
except KeyError:
|
| 213 |
+
pass
|
| 214 |
+
|
| 215 |
+
def add(self, key, val):
|
| 216 |
+
"""Adds a (name, value) pair, doesn't overwrite the value if it already
|
| 217 |
+
exists.
|
| 218 |
+
|
| 219 |
+
>>> headers = HTTPHeaderDict(foo='bar')
|
| 220 |
+
>>> headers.add('Foo', 'baz')
|
| 221 |
+
>>> headers['foo']
|
| 222 |
+
'bar, baz'
|
| 223 |
+
"""
|
| 224 |
+
key_lower = key.lower()
|
| 225 |
+
new_vals = [key, val]
|
| 226 |
+
# Keep the common case aka no item present as fast as possible
|
| 227 |
+
vals = self._container.setdefault(key_lower, new_vals)
|
| 228 |
+
if new_vals is not vals:
|
| 229 |
+
vals.append(val)
|
| 230 |
+
|
| 231 |
+
def extend(self, *args, **kwargs):
|
| 232 |
+
"""Generic import function for any type of header-like object.
|
| 233 |
+
Adapted version of MutableMapping.update in order to insert items
|
| 234 |
+
with self.add instead of self.__setitem__
|
| 235 |
+
"""
|
| 236 |
+
if len(args) > 1:
|
| 237 |
+
raise TypeError(
|
| 238 |
+
"extend() takes at most 1 positional "
|
| 239 |
+
"arguments ({0} given)".format(len(args))
|
| 240 |
+
)
|
| 241 |
+
other = args[0] if len(args) >= 1 else ()
|
| 242 |
+
|
| 243 |
+
if isinstance(other, HTTPHeaderDict):
|
| 244 |
+
for key, val in other.iteritems():
|
| 245 |
+
self.add(key, val)
|
| 246 |
+
elif isinstance(other, Mapping):
|
| 247 |
+
for key in other:
|
| 248 |
+
self.add(key, other[key])
|
| 249 |
+
elif hasattr(other, "keys"):
|
| 250 |
+
for key in other.keys():
|
| 251 |
+
self.add(key, other[key])
|
| 252 |
+
else:
|
| 253 |
+
for key, value in other:
|
| 254 |
+
self.add(key, value)
|
| 255 |
+
|
| 256 |
+
for key, value in kwargs.items():
|
| 257 |
+
self.add(key, value)
|
| 258 |
+
|
| 259 |
+
def getlist(self, key, default=__marker):
|
| 260 |
+
"""Returns a list of all the values for the named field. Returns an
|
| 261 |
+
empty list if the key doesn't exist."""
|
| 262 |
+
try:
|
| 263 |
+
vals = self._container[key.lower()]
|
| 264 |
+
except KeyError:
|
| 265 |
+
if default is self.__marker:
|
| 266 |
+
return []
|
| 267 |
+
return default
|
| 268 |
+
else:
|
| 269 |
+
return vals[1:]
|
| 270 |
+
|
| 271 |
+
def _prepare_for_method_change(self):
|
| 272 |
+
"""
|
| 273 |
+
Remove content-specific header fields before changing the request
|
| 274 |
+
method to GET or HEAD according to RFC 9110, Section 15.4.
|
| 275 |
+
"""
|
| 276 |
+
content_specific_headers = [
|
| 277 |
+
"Content-Encoding",
|
| 278 |
+
"Content-Language",
|
| 279 |
+
"Content-Location",
|
| 280 |
+
"Content-Type",
|
| 281 |
+
"Content-Length",
|
| 282 |
+
"Digest",
|
| 283 |
+
"Last-Modified",
|
| 284 |
+
]
|
| 285 |
+
for header in content_specific_headers:
|
| 286 |
+
self.discard(header)
|
| 287 |
+
return self
|
| 288 |
+
|
| 289 |
+
# Backwards compatibility for httplib
|
| 290 |
+
getheaders = getlist
|
| 291 |
+
getallmatchingheaders = getlist
|
| 292 |
+
iget = getlist
|
| 293 |
+
|
| 294 |
+
# Backwards compatibility for http.cookiejar
|
| 295 |
+
get_all = getlist
|
| 296 |
+
|
| 297 |
+
def __repr__(self):
|
| 298 |
+
return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
|
| 299 |
+
|
| 300 |
+
def _copy_from(self, other):
|
| 301 |
+
for key in other:
|
| 302 |
+
val = other.getlist(key)
|
| 303 |
+
if isinstance(val, list):
|
| 304 |
+
# Don't need to convert tuples
|
| 305 |
+
val = list(val)
|
| 306 |
+
self._container[key.lower()] = [key] + val
|
| 307 |
+
|
| 308 |
+
def copy(self):
|
| 309 |
+
clone = type(self)()
|
| 310 |
+
clone._copy_from(self)
|
| 311 |
+
return clone
|
| 312 |
+
|
| 313 |
+
def iteritems(self):
|
| 314 |
+
"""Iterate over all header lines, including duplicate ones."""
|
| 315 |
+
for key in self:
|
| 316 |
+
vals = self._container[key.lower()]
|
| 317 |
+
for val in vals[1:]:
|
| 318 |
+
yield vals[0], val
|
| 319 |
+
|
| 320 |
+
def itermerged(self):
|
| 321 |
+
"""Iterate over all headers, merging duplicate ones together."""
|
| 322 |
+
for key in self:
|
| 323 |
+
val = self._container[key.lower()]
|
| 324 |
+
yield val[0], ", ".join(val[1:])
|
| 325 |
+
|
| 326 |
+
def items(self):
|
| 327 |
+
return list(self.iteritems())
|
| 328 |
+
|
| 329 |
+
@classmethod
|
| 330 |
+
def from_httplib(cls, message): # Python 2
|
| 331 |
+
"""Read headers from a Python 2 httplib message object."""
|
| 332 |
+
# python2.7 does not expose a proper API for exporting multiheaders
|
| 333 |
+
# efficiently. This function re-reads raw lines from the message
|
| 334 |
+
# object and extracts the multiheaders properly.
|
| 335 |
+
obs_fold_continued_leaders = (" ", "\t")
|
| 336 |
+
headers = []
|
| 337 |
+
|
| 338 |
+
for line in message.headers:
|
| 339 |
+
if line.startswith(obs_fold_continued_leaders):
|
| 340 |
+
if not headers:
|
| 341 |
+
# We received a header line that starts with OWS as described
|
| 342 |
+
# in RFC-7230 S3.2.4. This indicates a multiline header, but
|
| 343 |
+
# there exists no previous header to which we can attach it.
|
| 344 |
+
raise InvalidHeader(
|
| 345 |
+
"Header continuation with no previous header: %s" % line
|
| 346 |
+
)
|
| 347 |
+
else:
|
| 348 |
+
key, value = headers[-1]
|
| 349 |
+
headers[-1] = (key, value + " " + line.strip())
|
| 350 |
+
continue
|
| 351 |
+
|
| 352 |
+
key, value = line.split(":", 1)
|
| 353 |
+
headers.append((key, value.strip()))
|
| 354 |
+
|
| 355 |
+
return cls(headers)
|
lib/python3.12/site-packages/pip/_vendor/urllib3/_version.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file is protected via CODEOWNERS
|
| 2 |
+
__version__ = "1.26.20"
|
lib/python3.12/site-packages/pip/_vendor/urllib3/connection.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import absolute_import
|
| 2 |
+
|
| 3 |
+
import datetime
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import socket
|
| 8 |
+
import warnings
|
| 9 |
+
from socket import error as SocketError
|
| 10 |
+
from socket import timeout as SocketTimeout
|
| 11 |
+
|
| 12 |
+
from .packages import six
|
| 13 |
+
from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection
|
| 14 |
+
from .packages.six.moves.http_client import HTTPException # noqa: F401
|
| 15 |
+
from .util.proxy import create_proxy_ssl_context
|
| 16 |
+
|
| 17 |
+
try: # Compiled with SSL?
|
| 18 |
+
import ssl
|
| 19 |
+
|
| 20 |
+
BaseSSLError = ssl.SSLError
|
| 21 |
+
except (ImportError, AttributeError): # Platform-specific: No SSL.
|
| 22 |
+
ssl = None
|
| 23 |
+
|
| 24 |
+
class BaseSSLError(BaseException):
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
# Python 3: not a no-op, we're adding this to the namespace so it can be imported.
|
| 30 |
+
ConnectionError = ConnectionError
|
| 31 |
+
except NameError:
|
| 32 |
+
# Python 2
|
| 33 |
+
class ConnectionError(Exception):
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
try: # Python 3:
|
| 38 |
+
# Not a no-op, we're adding this to the namespace so it can be imported.
|
| 39 |
+
BrokenPipeError = BrokenPipeError
|
| 40 |
+
except NameError: # Python 2:
|
| 41 |
+
|
| 42 |
+
class BrokenPipeError(Exception):
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
from ._collections import HTTPHeaderDict # noqa (historical, removed in v2)
|
| 47 |
+
from ._version import __version__
|
| 48 |
+
from .exceptions import (
|
| 49 |
+
ConnectTimeoutError,
|
| 50 |
+
NewConnectionError,
|
| 51 |
+
SubjectAltNameWarning,
|
| 52 |
+
SystemTimeWarning,
|
| 53 |
+
)
|
| 54 |
+
from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection
|
| 55 |
+
from .util.ssl_ import (
|
| 56 |
+
assert_fingerprint,
|
| 57 |
+
create_urllib3_context,
|
| 58 |
+
is_ipaddress,
|
| 59 |
+
resolve_cert_reqs,
|
| 60 |
+
resolve_ssl_version,
|
| 61 |
+
ssl_wrap_socket,
|
| 62 |
+
)
|
| 63 |
+
from .util.ssl_match_hostname import CertificateError, match_hostname
|
| 64 |
+
|
| 65 |
+
log = logging.getLogger(__name__)
|
| 66 |
+
|
| 67 |
+
port_by_scheme = {"http": 80, "https": 443}
|
| 68 |
+
|
| 69 |
+
# When it comes time to update this value as a part of regular maintenance
|
| 70 |
+
# (ie test_recent_date is failing) update it to ~6 months before the current date.
|
| 71 |
+
RECENT_DATE = datetime.date(2024, 1, 1)
|
| 72 |
+
|
| 73 |
+
_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class HTTPConnection(_HTTPConnection, object):
|
| 77 |
+
"""
|
| 78 |
+
Based on :class:`http.client.HTTPConnection` but provides an extra constructor
|
| 79 |
+
backwards-compatibility layer between older and newer Pythons.
|
| 80 |
+
|
| 81 |
+
Additional keyword parameters are used to configure attributes of the connection.
|
| 82 |
+
Accepted parameters include:
|
| 83 |
+
|
| 84 |
+
- ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool`
|
| 85 |
+
- ``source_address``: Set the source address for the current connection.
|
| 86 |
+
- ``socket_options``: Set specific options on the underlying socket. If not specified, then
|
| 87 |
+
defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling
|
| 88 |
+
Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy.
|
| 89 |
+
|
| 90 |
+
For example, if you wish to enable TCP Keep Alive in addition to the defaults,
|
| 91 |
+
you might pass:
|
| 92 |
+
|
| 93 |
+
.. code-block:: python
|
| 94 |
+
|
| 95 |
+
HTTPConnection.default_socket_options + [
|
| 96 |
+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
Or you may want to disable the defaults by passing an empty list (e.g., ``[]``).
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
default_port = port_by_scheme["http"]
|
| 103 |
+
|
| 104 |
+
#: Disable Nagle's algorithm by default.
|
| 105 |
+
#: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]``
|
| 106 |
+
default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
|
| 107 |
+
|
| 108 |
+
#: Whether this connection verifies the host's certificate.
|
| 109 |
+
is_verified = False
|
| 110 |
+
|
| 111 |
+
#: Whether this proxy connection (if used) verifies the proxy host's
|
| 112 |
+
#: certificate.
|
| 113 |
+
proxy_is_verified = None
|
| 114 |
+
|
| 115 |
+
def __init__(self, *args, **kw):
|
| 116 |
+
if not six.PY2:
|
| 117 |
+
kw.pop("strict", None)
|
| 118 |
+
|
| 119 |
+
# Pre-set source_address.
|
| 120 |
+
self.source_address = kw.get("source_address")
|
| 121 |
+
|
| 122 |
+
#: The socket options provided by the user. If no options are
|
| 123 |
+
#: provided, we use the default options.
|
| 124 |
+
self.socket_options = kw.pop("socket_options", self.default_socket_options)
|
| 125 |
+
|
| 126 |
+
# Proxy options provided by the user.
|
| 127 |
+
self.proxy = kw.pop("proxy", None)
|
| 128 |
+
self.proxy_config = kw.pop("proxy_config", None)
|
| 129 |
+
|
| 130 |
+
_HTTPConnection.__init__(self, *args, **kw)
|
| 131 |
+
|
| 132 |
+
@property
|
| 133 |
+
def host(self):
|
| 134 |
+
"""
|
| 135 |
+
Getter method to remove any trailing dots that indicate the hostname is an FQDN.
|
| 136 |
+
|
| 137 |
+
In general, SSL certificates don't include the trailing dot indicating a
|
| 138 |
+
fully-qualified domain name, and thus, they don't validate properly when
|
| 139 |
+
checked against a domain name that includes the dot. In addition, some
|
| 140 |
+
servers may not expect to receive the trailing dot when provided.
|
| 141 |
+
|
| 142 |
+
However, the hostname with trailing dot is critical to DNS resolution; doing a
|
| 143 |
+
lookup with the trailing dot will properly only resolve the appropriate FQDN,
|
| 144 |
+
whereas a lookup without a trailing dot will search the system's search domain
|
| 145 |
+
list. Thus, it's important to keep the original host around for use only in
|
| 146 |
+
those cases where it's appropriate (i.e., when doing DNS lookup to establish the
|
| 147 |
+
actual TCP connection across which we're going to send HTTP requests).
|
| 148 |
+
"""
|
| 149 |
+
return self._dns_host.rstrip(".")
|
| 150 |
+
|
| 151 |
+
@host.setter
|
| 152 |
+
def host(self, value):
|
| 153 |
+
"""
|
| 154 |
+
Setter for the `host` property.
|
| 155 |
+
|
| 156 |
+
We assume that only urllib3 uses the _dns_host attribute; httplib itself
|
| 157 |
+
only uses `host`, and it seems reasonable that other libraries follow suit.
|
| 158 |
+
"""
|
| 159 |
+
self._dns_host = value
|
| 160 |
+
|
| 161 |
+
def _new_conn(self):
|
| 162 |
+
"""Establish a socket connection and set nodelay settings on it.
|
| 163 |
+
|
| 164 |
+
:return: New socket connection.
|
| 165 |
+
"""
|
| 166 |
+
extra_kw = {}
|
| 167 |
+
if self.source_address:
|
| 168 |
+
extra_kw["source_address"] = self.source_address
|
| 169 |
+
|
| 170 |
+
if self.socket_options:
|
| 171 |
+
extra_kw["socket_options"] = self.socket_options
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
conn = connection.create_connection(
|
| 175 |
+
(self._dns_host, self.port), self.timeout, **extra_kw
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
except SocketTimeout:
|
| 179 |
+
raise ConnectTimeoutError(
|
| 180 |
+
self,
|
| 181 |
+
"Connection to %s timed out. (connect timeout=%s)"
|
| 182 |
+
% (self.host, self.timeout),
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
except SocketError as e:
|
| 186 |
+
raise NewConnectionError(
|
| 187 |
+
self, "Failed to establish a new connection: %s" % e
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
return conn
|
| 191 |
+
|
| 192 |
+
def _is_using_tunnel(self):
|
| 193 |
+
# Google App Engine's httplib does not define _tunnel_host
|
| 194 |
+
return getattr(self, "_tunnel_host", None)
|
| 195 |
+
|
| 196 |
+
def _prepare_conn(self, conn):
|
| 197 |
+
self.sock = conn
|
| 198 |
+
if self._is_using_tunnel():
|
| 199 |
+
# TODO: Fix tunnel so it doesn't depend on self.sock state.
|
| 200 |
+
self._tunnel()
|
| 201 |
+
# Mark this connection as not reusable
|
| 202 |
+
self.auto_open = 0
|
| 203 |
+
|
| 204 |
+
def connect(self):
|
| 205 |
+
conn = self._new_conn()
|
| 206 |
+
self._prepare_conn(conn)
|
| 207 |
+
|
| 208 |
+
def putrequest(self, method, url, *args, **kwargs):
|
| 209 |
+
""" """
|
| 210 |
+
# Empty docstring because the indentation of CPython's implementation
|
| 211 |
+
# is broken but we don't want this method in our documentation.
|
| 212 |
+
match = _CONTAINS_CONTROL_CHAR_RE.search(method)
|
| 213 |
+
if match:
|
| 214 |
+
raise ValueError(
|
| 215 |
+
"Method cannot contain non-token characters %r (found at least %r)"
|
| 216 |
+
% (method, match.group())
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
return _HTTPConnection.putrequest(self, method, url, *args, **kwargs)
|
| 220 |
+
|
| 221 |
+
def putheader(self, header, *values):
|
| 222 |
+
""" """
|
| 223 |
+
if not any(isinstance(v, str) and v == SKIP_HEADER for v in values):
|
| 224 |
+
_HTTPConnection.putheader(self, header, *values)
|
| 225 |
+
elif six.ensure_str(header.lower()) not in SKIPPABLE_HEADERS:
|
| 226 |
+
raise ValueError(
|
| 227 |
+
"urllib3.util.SKIP_HEADER only supports '%s'"
|
| 228 |
+
% ("', '".join(map(str.title, sorted(SKIPPABLE_HEADERS))),)
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def request(self, method, url, body=None, headers=None):
|
| 232 |
+
# Update the inner socket's timeout value to send the request.
|
| 233 |
+
# This only triggers if the connection is re-used.
|
| 234 |
+
if getattr(self, "sock", None) is not None:
|
| 235 |
+
self.sock.settimeout(self.timeout)
|
| 236 |
+
|
| 237 |
+
if headers is None:
|
| 238 |
+
headers = {}
|
| 239 |
+
else:
|
| 240 |
+
# Avoid modifying the headers passed into .request()
|
| 241 |
+
headers = headers.copy()
|
| 242 |
+
if "user-agent" not in (six.ensure_str(k.lower()) for k in headers):
|
| 243 |
+
headers["User-Agent"] = _get_default_user_agent()
|
| 244 |
+
super(HTTPConnection, self).request(method, url, body=body, headers=headers)
|
| 245 |
+
|
| 246 |
+
def request_chunked(self, method, url, body=None, headers=None):
|
| 247 |
+
"""
|
| 248 |
+
Alternative to the common request method, which sends the
|
| 249 |
+
body with chunked encoding and not as one block
|
| 250 |
+
"""
|
| 251 |
+
headers = headers or {}
|
| 252 |
+
header_keys = set([six.ensure_str(k.lower()) for k in headers])
|
| 253 |
+
skip_accept_encoding = "accept-encoding" in header_keys
|
| 254 |
+
skip_host = "host" in header_keys
|
| 255 |
+
self.putrequest(
|
| 256 |
+
method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host
|
| 257 |
+
)
|
| 258 |
+
if "user-agent" not in header_keys:
|
| 259 |
+
self.putheader("User-Agent", _get_default_user_agent())
|
| 260 |
+
for header, value in headers.items():
|
| 261 |
+
self.putheader(header, value)
|
| 262 |
+
if "transfer-encoding" not in header_keys:
|
| 263 |
+
self.putheader("Transfer-Encoding", "chunked")
|
| 264 |
+
self.endheaders()
|
| 265 |
+
|
| 266 |
+
if body is not None:
|
| 267 |
+
stringish_types = six.string_types + (bytes,)
|
| 268 |
+
if isinstance(body, stringish_types):
|
| 269 |
+
body = (body,)
|
| 270 |
+
for chunk in body:
|
| 271 |
+
if not chunk:
|
| 272 |
+
continue
|
| 273 |
+
if not isinstance(chunk, bytes):
|
| 274 |
+
chunk = chunk.encode("utf8")
|
| 275 |
+
len_str = hex(len(chunk))[2:]
|
| 276 |
+
to_send = bytearray(len_str.encode())
|
| 277 |
+
to_send += b"\r\n"
|
| 278 |
+
to_send += chunk
|
| 279 |
+
to_send += b"\r\n"
|
| 280 |
+
self.send(to_send)
|
| 281 |
+
|
| 282 |
+
# After the if clause, to always have a closed body
|
| 283 |
+
self.send(b"0\r\n\r\n")
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
class HTTPSConnection(HTTPConnection):
|
| 287 |
+
"""
|
| 288 |
+
Many of the parameters to this constructor are passed to the underlying SSL
|
| 289 |
+
socket by means of :py:func:`urllib3.util.ssl_wrap_socket`.
|
| 290 |
+
"""
|
| 291 |
+
|
| 292 |
+
default_port = port_by_scheme["https"]
|
| 293 |
+
|
| 294 |
+
cert_reqs = None
|
| 295 |
+
ca_certs = None
|
| 296 |
+
ca_cert_dir = None
|
| 297 |
+
ca_cert_data = None
|
| 298 |
+
ssl_version = None
|
| 299 |
+
assert_fingerprint = None
|
| 300 |
+
tls_in_tls_required = False
|
| 301 |
+
|
| 302 |
+
def __init__(
|
| 303 |
+
self,
|
| 304 |
+
host,
|
| 305 |
+
port=None,
|
| 306 |
+
key_file=None,
|
| 307 |
+
cert_file=None,
|
| 308 |
+
key_password=None,
|
| 309 |
+
strict=None,
|
| 310 |
+
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
| 311 |
+
ssl_context=None,
|
| 312 |
+
server_hostname=None,
|
| 313 |
+
**kw
|
| 314 |
+
):
|
| 315 |
+
|
| 316 |
+
HTTPConnection.__init__(self, host, port, strict=strict, timeout=timeout, **kw)
|
| 317 |
+
|
| 318 |
+
self.key_file = key_file
|
| 319 |
+
self.cert_file = cert_file
|
| 320 |
+
self.key_password = key_password
|
| 321 |
+
self.ssl_context = ssl_context
|
| 322 |
+
self.server_hostname = server_hostname
|
| 323 |
+
|
| 324 |
+
# Required property for Google AppEngine 1.9.0 which otherwise causes
|
| 325 |
+
# HTTPS requests to go out as HTTP. (See Issue #356)
|
| 326 |
+
self._protocol = "https"
|
| 327 |
+
|
| 328 |
+
def set_cert(
|
| 329 |
+
self,
|
| 330 |
+
key_file=None,
|
| 331 |
+
cert_file=None,
|
| 332 |
+
cert_reqs=None,
|
| 333 |
+
key_password=None,
|
| 334 |
+
ca_certs=None,
|
| 335 |
+
assert_hostname=None,
|
| 336 |
+
assert_fingerprint=None,
|
| 337 |
+
ca_cert_dir=None,
|
| 338 |
+
ca_cert_data=None,
|
| 339 |
+
):
|
| 340 |
+
"""
|
| 341 |
+
This method should only be called once, before the connection is used.
|
| 342 |
+
"""
|
| 343 |
+
# If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also
|
| 344 |
+
# have an SSLContext object in which case we'll use its verify_mode.
|
| 345 |
+
if cert_reqs is None:
|
| 346 |
+
if self.ssl_context is not None:
|
| 347 |
+
cert_reqs = self.ssl_context.verify_mode
|
| 348 |
+
else:
|
| 349 |
+
cert_reqs = resolve_cert_reqs(None)
|
| 350 |
+
|
| 351 |
+
self.key_file = key_file
|
| 352 |
+
self.cert_file = cert_file
|
| 353 |
+
self.cert_reqs = cert_reqs
|
| 354 |
+
self.key_password = key_password
|
| 355 |
+
self.assert_hostname = assert_hostname
|
| 356 |
+
self.assert_fingerprint = assert_fingerprint
|
| 357 |
+
self.ca_certs = ca_certs and os.path.expanduser(ca_certs)
|
| 358 |
+
self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir)
|
| 359 |
+
self.ca_cert_data = ca_cert_data
|
| 360 |
+
|
| 361 |
+
def connect(self):
|
| 362 |
+
# Add certificate verification
|
| 363 |
+
self.sock = conn = self._new_conn()
|
| 364 |
+
hostname = self.host
|
| 365 |
+
tls_in_tls = False
|
| 366 |
+
|
| 367 |
+
if self._is_using_tunnel():
|
| 368 |
+
if self.tls_in_tls_required:
|
| 369 |
+
self.sock = conn = self._connect_tls_proxy(hostname, conn)
|
| 370 |
+
tls_in_tls = True
|
| 371 |
+
|
| 372 |
+
# Calls self._set_hostport(), so self.host is
|
| 373 |
+
# self._tunnel_host below.
|
| 374 |
+
self._tunnel()
|
| 375 |
+
# Mark this connection as not reusable
|
| 376 |
+
self.auto_open = 0
|
| 377 |
+
|
| 378 |
+
# Override the host with the one we're requesting data from.
|
| 379 |
+
hostname = self._tunnel_host
|
| 380 |
+
|
| 381 |
+
server_hostname = hostname
|
| 382 |
+
if self.server_hostname is not None:
|
| 383 |
+
server_hostname = self.server_hostname
|
| 384 |
+
|
| 385 |
+
is_time_off = datetime.date.today() < RECENT_DATE
|
| 386 |
+
if is_time_off:
|
| 387 |
+
warnings.warn(
|
| 388 |
+
(
|
| 389 |
+
"System time is way off (before {0}). This will probably "
|
| 390 |
+
"lead to SSL verification errors"
|
| 391 |
+
).format(RECENT_DATE),
|
| 392 |
+
SystemTimeWarning,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
# Wrap socket using verification with the root certs in
|
| 396 |
+
# trusted_root_certs
|
| 397 |
+
default_ssl_context = False
|
| 398 |
+
if self.ssl_context is None:
|
| 399 |
+
default_ssl_context = True
|
| 400 |
+
self.ssl_context = create_urllib3_context(
|
| 401 |
+
ssl_version=resolve_ssl_version(self.ssl_version),
|
| 402 |
+
cert_reqs=resolve_cert_reqs(self.cert_reqs),
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
context = self.ssl_context
|
| 406 |
+
context.verify_mode = resolve_cert_reqs(self.cert_reqs)
|
| 407 |
+
|
| 408 |
+
# Try to load OS default certs if none are given.
|
| 409 |
+
# Works well on Windows (requires Python3.4+)
|
| 410 |
+
if (
|
| 411 |
+
not self.ca_certs
|
| 412 |
+
and not self.ca_cert_dir
|
| 413 |
+
and not self.ca_cert_data
|
| 414 |
+
and default_ssl_context
|
| 415 |
+
and hasattr(context, "load_default_certs")
|
| 416 |
+
):
|
| 417 |
+
context.load_default_certs()
|
| 418 |
+
|
| 419 |
+
self.sock = ssl_wrap_socket(
|
| 420 |
+
sock=conn,
|
| 421 |
+
keyfile=self.key_file,
|
| 422 |
+
certfile=self.cert_file,
|
| 423 |
+
key_password=self.key_password,
|
| 424 |
+
ca_certs=self.ca_certs,
|
| 425 |
+
ca_cert_dir=self.ca_cert_dir,
|
| 426 |
+
ca_cert_data=self.ca_cert_data,
|
| 427 |
+
server_hostname=server_hostname,
|
| 428 |
+
ssl_context=context,
|
| 429 |
+
tls_in_tls=tls_in_tls,
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
# If we're using all defaults and the connection
|
| 433 |
+
# is TLSv1 or TLSv1.1 we throw a DeprecationWarning
|
| 434 |
+
# for the host.
|
| 435 |
+
if (
|
| 436 |
+
default_ssl_context
|
| 437 |
+
and self.ssl_version is None
|
| 438 |
+
and hasattr(self.sock, "version")
|
| 439 |
+
and self.sock.version() in {"TLSv1", "TLSv1.1"}
|
| 440 |
+
): # Defensive:
|
| 441 |
+
warnings.warn(
|
| 442 |
+
"Negotiating TLSv1/TLSv1.1 by default is deprecated "
|
| 443 |
+
"and will be disabled in urllib3 v2.0.0. Connecting to "
|
| 444 |
+
"'%s' with '%s' can be enabled by explicitly opting-in "
|
| 445 |
+
"with 'ssl_version'" % (self.host, self.sock.version()),
|
| 446 |
+
DeprecationWarning,
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
if self.assert_fingerprint:
|
| 450 |
+
assert_fingerprint(
|
| 451 |
+
self.sock.getpeercert(binary_form=True), self.assert_fingerprint
|
| 452 |
+
)
|
| 453 |
+
elif (
|
| 454 |
+
context.verify_mode != ssl.CERT_NONE
|
| 455 |
+
and not getattr(context, "check_hostname", False)
|
| 456 |
+
and self.assert_hostname is not False
|
| 457 |
+
):
|
| 458 |
+
# While urllib3 attempts to always turn off hostname matching from
|
| 459 |
+
# the TLS library, this cannot always be done. So we check whether
|
| 460 |
+
# the TLS Library still thinks it's matching hostnames.
|
| 461 |
+
cert = self.sock.getpeercert()
|
| 462 |
+
if not cert.get("subjectAltName", ()):
|
| 463 |
+
warnings.warn(
|
| 464 |
+
(
|
| 465 |
+
"Certificate for {0} has no `subjectAltName`, falling back to check for a "
|
| 466 |
+
"`commonName` for now. This feature is being removed by major browsers and "
|
| 467 |
+
"deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 "
|
| 468 |
+
"for details.)".format(hostname)
|
| 469 |
+
),
|
| 470 |
+
SubjectAltNameWarning,
|
| 471 |
+
)
|
| 472 |
+
_match_hostname(cert, self.assert_hostname or server_hostname)
|
| 473 |
+
|
| 474 |
+
self.is_verified = (
|
| 475 |
+
context.verify_mode == ssl.CERT_REQUIRED
|
| 476 |
+
or self.assert_fingerprint is not None
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
def _connect_tls_proxy(self, hostname, conn):
|
| 480 |
+
"""
|
| 481 |
+
Establish a TLS connection to the proxy using the provided SSL context.
|
| 482 |
+
"""
|
| 483 |
+
proxy_config = self.proxy_config
|
| 484 |
+
ssl_context = proxy_config.ssl_context
|
| 485 |
+
if ssl_context:
|
| 486 |
+
# If the user provided a proxy context, we assume CA and client
|
| 487 |
+
# certificates have already been set
|
| 488 |
+
return ssl_wrap_socket(
|
| 489 |
+
sock=conn,
|
| 490 |
+
server_hostname=hostname,
|
| 491 |
+
ssl_context=ssl_context,
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
ssl_context = create_proxy_ssl_context(
|
| 495 |
+
self.ssl_version,
|
| 496 |
+
self.cert_reqs,
|
| 497 |
+
self.ca_certs,
|
| 498 |
+
self.ca_cert_dir,
|
| 499 |
+
self.ca_cert_data,
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
# If no cert was provided, use only the default options for server
|
| 503 |
+
# certificate validation
|
| 504 |
+
socket = ssl_wrap_socket(
|
| 505 |
+
sock=conn,
|
| 506 |
+
ca_certs=self.ca_certs,
|
| 507 |
+
ca_cert_dir=self.ca_cert_dir,
|
| 508 |
+
ca_cert_data=self.ca_cert_data,
|
| 509 |
+
server_hostname=hostname,
|
| 510 |
+
ssl_context=ssl_context,
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
if ssl_context.verify_mode != ssl.CERT_NONE and not getattr(
|
| 514 |
+
ssl_context, "check_hostname", False
|
| 515 |
+
):
|
| 516 |
+
# While urllib3 attempts to always turn off hostname matching from
|
| 517 |
+
# the TLS library, this cannot always be done. So we check whether
|
| 518 |
+
# the TLS Library still thinks it's matching hostnames.
|
| 519 |
+
cert = socket.getpeercert()
|
| 520 |
+
if not cert.get("subjectAltName", ()):
|
| 521 |
+
warnings.warn(
|
| 522 |
+
(
|
| 523 |
+
"Certificate for {0} has no `subjectAltName`, falling back to check for a "
|
| 524 |
+
"`commonName` for now. This feature is being removed by major browsers and "
|
| 525 |
+
"deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 "
|
| 526 |
+
"for details.)".format(hostname)
|
| 527 |
+
),
|
| 528 |
+
SubjectAltNameWarning,
|
| 529 |
+
)
|
| 530 |
+
_match_hostname(cert, hostname)
|
| 531 |
+
|
| 532 |
+
self.proxy_is_verified = ssl_context.verify_mode == ssl.CERT_REQUIRED
|
| 533 |
+
return socket
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def _match_hostname(cert, asserted_hostname):
|
| 537 |
+
# Our upstream implementation of ssl.match_hostname()
|
| 538 |
+
# only applies this normalization to IP addresses so it doesn't
|
| 539 |
+
# match DNS SANs so we do the same thing!
|
| 540 |
+
stripped_hostname = asserted_hostname.strip("u[]")
|
| 541 |
+
if is_ipaddress(stripped_hostname):
|
| 542 |
+
asserted_hostname = stripped_hostname
|
| 543 |
+
|
| 544 |
+
try:
|
| 545 |
+
match_hostname(cert, asserted_hostname)
|
| 546 |
+
except CertificateError as e:
|
| 547 |
+
log.warning(
|
| 548 |
+
"Certificate did not match expected hostname: %s. Certificate: %s",
|
| 549 |
+
asserted_hostname,
|
| 550 |
+
cert,
|
| 551 |
+
)
|
| 552 |
+
# Add cert to exception and reraise so client code can inspect
|
| 553 |
+
# the cert when catching the exception, if they want to
|
| 554 |
+
e._peer_cert = cert
|
| 555 |
+
raise
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
def _get_default_user_agent():
|
| 559 |
+
return "python-urllib3/%s" % __version__
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
class DummyConnection(object):
|
| 563 |
+
"""Used to detect a failed ConnectionCls import."""
|
| 564 |
+
|
| 565 |
+
pass
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
if not ssl:
|
| 569 |
+
HTTPSConnection = DummyConnection # noqa: F811
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
VerifiedHTTPSConnection = HTTPSConnection
|
lib/python3.12/site-packages/pip/_vendor/urllib3/connectionpool.py
ADDED
|
@@ -0,0 +1,1140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import absolute_import
|
| 2 |
+
|
| 3 |
+
import errno
|
| 4 |
+
import logging
|
| 5 |
+
import re
|
| 6 |
+
import socket
|
| 7 |
+
import sys
|
| 8 |
+
import warnings
|
| 9 |
+
from socket import error as SocketError
|
| 10 |
+
from socket import timeout as SocketTimeout
|
| 11 |
+
|
| 12 |
+
from ._collections import HTTPHeaderDict
|
| 13 |
+
from .connection import (
|
| 14 |
+
BaseSSLError,
|
| 15 |
+
BrokenPipeError,
|
| 16 |
+
DummyConnection,
|
| 17 |
+
HTTPConnection,
|
| 18 |
+
HTTPException,
|
| 19 |
+
HTTPSConnection,
|
| 20 |
+
VerifiedHTTPSConnection,
|
| 21 |
+
port_by_scheme,
|
| 22 |
+
)
|
| 23 |
+
from .exceptions import (
|
| 24 |
+
ClosedPoolError,
|
| 25 |
+
EmptyPoolError,
|
| 26 |
+
HeaderParsingError,
|
| 27 |
+
HostChangedError,
|
| 28 |
+
InsecureRequestWarning,
|
| 29 |
+
LocationValueError,
|
| 30 |
+
MaxRetryError,
|
| 31 |
+
NewConnectionError,
|
| 32 |
+
ProtocolError,
|
| 33 |
+
ProxyError,
|
| 34 |
+
ReadTimeoutError,
|
| 35 |
+
SSLError,
|
| 36 |
+
TimeoutError,
|
| 37 |
+
)
|
| 38 |
+
from .packages import six
|
| 39 |
+
from .packages.six.moves import queue
|
| 40 |
+
from .request import RequestMethods
|
| 41 |
+
from .response import HTTPResponse
|
| 42 |
+
from .util.connection import is_connection_dropped
|
| 43 |
+
from .util.proxy import connection_requires_http_tunnel
|
| 44 |
+
from .util.queue import LifoQueue
|
| 45 |
+
from .util.request import set_file_position
|
| 46 |
+
from .util.response import assert_header_parsing
|
| 47 |
+
from .util.retry import Retry
|
| 48 |
+
from .util.ssl_match_hostname import CertificateError
|
| 49 |
+
from .util.timeout import Timeout
|
| 50 |
+
from .util.url import Url, _encode_target
|
| 51 |
+
from .util.url import _normalize_host as normalize_host
|
| 52 |
+
from .util.url import get_host, parse_url
|
| 53 |
+
|
| 54 |
+
try: # Platform-specific: Python 3
|
| 55 |
+
import weakref
|
| 56 |
+
|
| 57 |
+
weakref_finalize = weakref.finalize
|
| 58 |
+
except AttributeError: # Platform-specific: Python 2
|
| 59 |
+
from .packages.backports.weakref_finalize import weakref_finalize
|
| 60 |
+
|
| 61 |
+
xrange = six.moves.xrange
|
| 62 |
+
|
| 63 |
+
log = logging.getLogger(__name__)
|
| 64 |
+
|
| 65 |
+
_Default = object()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# Pool objects
|
| 69 |
+
class ConnectionPool(object):
|
| 70 |
+
"""
|
| 71 |
+
Base class for all connection pools, such as
|
| 72 |
+
:class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`.
|
| 73 |
+
|
| 74 |
+
.. note::
|
| 75 |
+
ConnectionPool.urlopen() does not normalize or percent-encode target URIs
|
| 76 |
+
which is useful if your target server doesn't support percent-encoded
|
| 77 |
+
target URIs.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
scheme = None
|
| 81 |
+
QueueCls = LifoQueue
|
| 82 |
+
|
| 83 |
+
def __init__(self, host, port=None):
|
| 84 |
+
if not host:
|
| 85 |
+
raise LocationValueError("No host specified.")
|
| 86 |
+
|
| 87 |
+
self.host = _normalize_host(host, scheme=self.scheme)
|
| 88 |
+
self._proxy_host = host.lower()
|
| 89 |
+
self.port = port
|
| 90 |
+
|
| 91 |
+
def __str__(self):
|
| 92 |
+
return "%s(host=%r, port=%r)" % (type(self).__name__, self.host, self.port)
|
| 93 |
+
|
| 94 |
+
def __enter__(self):
|
| 95 |
+
return self
|
| 96 |
+
|
| 97 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 98 |
+
self.close()
|
| 99 |
+
# Return False to re-raise any potential exceptions
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
def close(self):
|
| 103 |
+
"""
|
| 104 |
+
Close all pooled connections and disable the pool.
|
| 105 |
+
"""
|
| 106 |
+
pass
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252
|
| 110 |
+
_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
| 114 |
+
"""
|
| 115 |
+
Thread-safe connection pool for one host.
|
| 116 |
+
|
| 117 |
+
:param host:
|
| 118 |
+
Host used for this HTTP Connection (e.g. "localhost"), passed into
|
| 119 |
+
:class:`http.client.HTTPConnection`.
|
| 120 |
+
|
| 121 |
+
:param port:
|
| 122 |
+
Port used for this HTTP Connection (None is equivalent to 80), passed
|
| 123 |
+
into :class:`http.client.HTTPConnection`.
|
| 124 |
+
|
| 125 |
+
:param strict:
|
| 126 |
+
Causes BadStatusLine to be raised if the status line can't be parsed
|
| 127 |
+
as a valid HTTP/1.0 or 1.1 status line, passed into
|
| 128 |
+
:class:`http.client.HTTPConnection`.
|
| 129 |
+
|
| 130 |
+
.. note::
|
| 131 |
+
Only works in Python 2. This parameter is ignored in Python 3.
|
| 132 |
+
|
| 133 |
+
:param timeout:
|
| 134 |
+
Socket timeout in seconds for each individual connection. This can
|
| 135 |
+
be a float or integer, which sets the timeout for the HTTP request,
|
| 136 |
+
or an instance of :class:`urllib3.util.Timeout` which gives you more
|
| 137 |
+
fine-grained control over request timeouts. After the constructor has
|
| 138 |
+
been parsed, this is always a `urllib3.util.Timeout` object.
|
| 139 |
+
|
| 140 |
+
:param maxsize:
|
| 141 |
+
Number of connections to save that can be reused. More than 1 is useful
|
| 142 |
+
in multithreaded situations. If ``block`` is set to False, more
|
| 143 |
+
connections will be created but they will not be saved once they've
|
| 144 |
+
been used.
|
| 145 |
+
|
| 146 |
+
:param block:
|
| 147 |
+
If set to True, no more than ``maxsize`` connections will be used at
|
| 148 |
+
a time. When no free connections are available, the call will block
|
| 149 |
+
until a connection has been released. This is a useful side effect for
|
| 150 |
+
particular multithreaded situations where one does not want to use more
|
| 151 |
+
than maxsize connections per host to prevent flooding.
|
| 152 |
+
|
| 153 |
+
:param headers:
|
| 154 |
+
Headers to include with all requests, unless other headers are given
|
| 155 |
+
explicitly.
|
| 156 |
+
|
| 157 |
+
:param retries:
|
| 158 |
+
Retry configuration to use by default with requests in this pool.
|
| 159 |
+
|
| 160 |
+
:param _proxy:
|
| 161 |
+
Parsed proxy URL, should not be used directly, instead, see
|
| 162 |
+
:class:`urllib3.ProxyManager`
|
| 163 |
+
|
| 164 |
+
:param _proxy_headers:
|
| 165 |
+
A dictionary with proxy headers, should not be used directly,
|
| 166 |
+
instead, see :class:`urllib3.ProxyManager`
|
| 167 |
+
|
| 168 |
+
:param \\**conn_kw:
|
| 169 |
+
Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`,
|
| 170 |
+
:class:`urllib3.connection.HTTPSConnection` instances.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
scheme = "http"
|
| 174 |
+
ConnectionCls = HTTPConnection
|
| 175 |
+
ResponseCls = HTTPResponse
|
| 176 |
+
|
| 177 |
+
def __init__(
|
| 178 |
+
self,
|
| 179 |
+
host,
|
| 180 |
+
port=None,
|
| 181 |
+
strict=False,
|
| 182 |
+
timeout=Timeout.DEFAULT_TIMEOUT,
|
| 183 |
+
maxsize=1,
|
| 184 |
+
block=False,
|
| 185 |
+
headers=None,
|
| 186 |
+
retries=None,
|
| 187 |
+
_proxy=None,
|
| 188 |
+
_proxy_headers=None,
|
| 189 |
+
_proxy_config=None,
|
| 190 |
+
**conn_kw
|
| 191 |
+
):
|
| 192 |
+
ConnectionPool.__init__(self, host, port)
|
| 193 |
+
RequestMethods.__init__(self, headers)
|
| 194 |
+
|
| 195 |
+
self.strict = strict
|
| 196 |
+
|
| 197 |
+
if not isinstance(timeout, Timeout):
|
| 198 |
+
timeout = Timeout.from_float(timeout)
|
| 199 |
+
|
| 200 |
+
if retries is None:
|
| 201 |
+
retries = Retry.DEFAULT
|
| 202 |
+
|
| 203 |
+
self.timeout = timeout
|
| 204 |
+
self.retries = retries
|
| 205 |
+
|
| 206 |
+
self.pool = self.QueueCls(maxsize)
|
| 207 |
+
self.block = block
|
| 208 |
+
|
| 209 |
+
self.proxy = _proxy
|
| 210 |
+
self.proxy_headers = _proxy_headers or {}
|
| 211 |
+
self.proxy_config = _proxy_config
|
| 212 |
+
|
| 213 |
+
# Fill the queue up so that doing get() on it will block properly
|
| 214 |
+
for _ in xrange(maxsize):
|
| 215 |
+
self.pool.put(None)
|
| 216 |
+
|
| 217 |
+
# These are mostly for testing and debugging purposes.
|
| 218 |
+
self.num_connections = 0
|
| 219 |
+
self.num_requests = 0
|
| 220 |
+
self.conn_kw = conn_kw
|
| 221 |
+
|
| 222 |
+
if self.proxy:
|
| 223 |
+
# Enable Nagle's algorithm for proxies, to avoid packet fragmentation.
|
| 224 |
+
# We cannot know if the user has added default socket options, so we cannot replace the
|
| 225 |
+
# list.
|
| 226 |
+
self.conn_kw.setdefault("socket_options", [])
|
| 227 |
+
|
| 228 |
+
self.conn_kw["proxy"] = self.proxy
|
| 229 |
+
self.conn_kw["proxy_config"] = self.proxy_config
|
| 230 |
+
|
| 231 |
+
# Do not pass 'self' as callback to 'finalize'.
|
| 232 |
+
# Then the 'finalize' would keep an endless living (leak) to self.
|
| 233 |
+
# By just passing a reference to the pool allows the garbage collector
|
| 234 |
+
# to free self if nobody else has a reference to it.
|
| 235 |
+
pool = self.pool
|
| 236 |
+
|
| 237 |
+
# Close all the HTTPConnections in the pool before the
|
| 238 |
+
# HTTPConnectionPool object is garbage collected.
|
| 239 |
+
weakref_finalize(self, _close_pool_connections, pool)
|
| 240 |
+
|
| 241 |
+
def _new_conn(self):
|
| 242 |
+
"""
|
| 243 |
+
Return a fresh :class:`HTTPConnection`.
|
| 244 |
+
"""
|
| 245 |
+
self.num_connections += 1
|
| 246 |
+
log.debug(
|
| 247 |
+
"Starting new HTTP connection (%d): %s:%s",
|
| 248 |
+
self.num_connections,
|
| 249 |
+
self.host,
|
| 250 |
+
self.port or "80",
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
conn = self.ConnectionCls(
|
| 254 |
+
host=self.host,
|
| 255 |
+
port=self.port,
|
| 256 |
+
timeout=self.timeout.connect_timeout,
|
| 257 |
+
strict=self.strict,
|
| 258 |
+
**self.conn_kw
|
| 259 |
+
)
|
| 260 |
+
return conn
|
| 261 |
+
|
| 262 |
+
def _get_conn(self, timeout=None):
|
| 263 |
+
"""
|
| 264 |
+
Get a connection. Will return a pooled connection if one is available.
|
| 265 |
+
|
| 266 |
+
If no connections are available and :prop:`.block` is ``False``, then a
|
| 267 |
+
fresh connection is returned.
|
| 268 |
+
|
| 269 |
+
:param timeout:
|
| 270 |
+
Seconds to wait before giving up and raising
|
| 271 |
+
:class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and
|
| 272 |
+
:prop:`.block` is ``True``.
|
| 273 |
+
"""
|
| 274 |
+
conn = None
|
| 275 |
+
try:
|
| 276 |
+
conn = self.pool.get(block=self.block, timeout=timeout)
|
| 277 |
+
|
| 278 |
+
except AttributeError: # self.pool is None
|
| 279 |
+
raise ClosedPoolError(self, "Pool is closed.")
|
| 280 |
+
|
| 281 |
+
except queue.Empty:
|
| 282 |
+
if self.block:
|
| 283 |
+
raise EmptyPoolError(
|
| 284 |
+
self,
|
| 285 |
+
"Pool reached maximum size and no more connections are allowed.",
|
| 286 |
+
)
|
| 287 |
+
pass # Oh well, we'll create a new connection then
|
| 288 |
+
|
| 289 |
+
# If this is a persistent connection, check if it got disconnected
|
| 290 |
+
if conn and is_connection_dropped(conn):
|
| 291 |
+
log.debug("Resetting dropped connection: %s", self.host)
|
| 292 |
+
conn.close()
|
| 293 |
+
if getattr(conn, "auto_open", 1) == 0:
|
| 294 |
+
# This is a proxied connection that has been mutated by
|
| 295 |
+
# http.client._tunnel() and cannot be reused (since it would
|
| 296 |
+
# attempt to bypass the proxy)
|
| 297 |
+
conn = None
|
| 298 |
+
|
| 299 |
+
return conn or self._new_conn()
|
| 300 |
+
|
| 301 |
+
def _put_conn(self, conn):
|
| 302 |
+
"""
|
| 303 |
+
Put a connection back into the pool.
|
| 304 |
+
|
| 305 |
+
:param conn:
|
| 306 |
+
Connection object for the current host and port as returned by
|
| 307 |
+
:meth:`._new_conn` or :meth:`._get_conn`.
|
| 308 |
+
|
| 309 |
+
If the pool is already full, the connection is closed and discarded
|
| 310 |
+
because we exceeded maxsize. If connections are discarded frequently,
|
| 311 |
+
then maxsize should be increased.
|
| 312 |
+
|
| 313 |
+
If the pool is closed, then the connection will be closed and discarded.
|
| 314 |
+
"""
|
| 315 |
+
try:
|
| 316 |
+
self.pool.put(conn, block=False)
|
| 317 |
+
return # Everything is dandy, done.
|
| 318 |
+
except AttributeError:
|
| 319 |
+
# self.pool is None.
|
| 320 |
+
pass
|
| 321 |
+
except queue.Full:
|
| 322 |
+
# This should never happen if self.block == True
|
| 323 |
+
log.warning(
|
| 324 |
+
"Connection pool is full, discarding connection: %s. Connection pool size: %s",
|
| 325 |
+
self.host,
|
| 326 |
+
self.pool.qsize(),
|
| 327 |
+
)
|
| 328 |
+
# Connection never got put back into the pool, close it.
|
| 329 |
+
if conn:
|
| 330 |
+
conn.close()
|
| 331 |
+
|
| 332 |
+
def _validate_conn(self, conn):
|
| 333 |
+
"""
|
| 334 |
+
Called right before a request is made, after the socket is created.
|
| 335 |
+
"""
|
| 336 |
+
pass
|
| 337 |
+
|
| 338 |
+
def _prepare_proxy(self, conn):
|
| 339 |
+
# Nothing to do for HTTP connections.
|
| 340 |
+
pass
|
| 341 |
+
|
| 342 |
+
def _get_timeout(self, timeout):
|
| 343 |
+
"""Helper that always returns a :class:`urllib3.util.Timeout`"""
|
| 344 |
+
if timeout is _Default:
|
| 345 |
+
return self.timeout.clone()
|
| 346 |
+
|
| 347 |
+
if isinstance(timeout, Timeout):
|
| 348 |
+
return timeout.clone()
|
| 349 |
+
else:
|
| 350 |
+
# User passed us an int/float. This is for backwards compatibility,
|
| 351 |
+
# can be removed later
|
| 352 |
+
return Timeout.from_float(timeout)
|
| 353 |
+
|
| 354 |
+
def _raise_timeout(self, err, url, timeout_value):
|
| 355 |
+
"""Is the error actually a timeout? Will raise a ReadTimeout or pass"""
|
| 356 |
+
|
| 357 |
+
if isinstance(err, SocketTimeout):
|
| 358 |
+
raise ReadTimeoutError(
|
| 359 |
+
self, url, "Read timed out. (read timeout=%s)" % timeout_value
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
# See the above comment about EAGAIN in Python 3. In Python 2 we have
|
| 363 |
+
# to specifically catch it and throw the timeout error
|
| 364 |
+
if hasattr(err, "errno") and err.errno in _blocking_errnos:
|
| 365 |
+
raise ReadTimeoutError(
|
| 366 |
+
self, url, "Read timed out. (read timeout=%s)" % timeout_value
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# Catch possible read timeouts thrown as SSL errors. If not the
|
| 370 |
+
# case, rethrow the original. We need to do this because of:
|
| 371 |
+
# http://bugs.python.org/issue10272
|
| 372 |
+
if "timed out" in str(err) or "did not complete (read)" in str(
|
| 373 |
+
err
|
| 374 |
+
): # Python < 2.7.4
|
| 375 |
+
raise ReadTimeoutError(
|
| 376 |
+
self, url, "Read timed out. (read timeout=%s)" % timeout_value
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
def _make_request(
|
| 380 |
+
self, conn, method, url, timeout=_Default, chunked=False, **httplib_request_kw
|
| 381 |
+
):
|
| 382 |
+
"""
|
| 383 |
+
Perform a request on a given urllib connection object taken from our
|
| 384 |
+
pool.
|
| 385 |
+
|
| 386 |
+
:param conn:
|
| 387 |
+
a connection from one of our connection pools
|
| 388 |
+
|
| 389 |
+
:param timeout:
|
| 390 |
+
Socket timeout in seconds for the request. This can be a
|
| 391 |
+
float or integer, which will set the same timeout value for
|
| 392 |
+
the socket connect and the socket read, or an instance of
|
| 393 |
+
:class:`urllib3.util.Timeout`, which gives you more fine-grained
|
| 394 |
+
control over your timeouts.
|
| 395 |
+
"""
|
| 396 |
+
self.num_requests += 1
|
| 397 |
+
|
| 398 |
+
timeout_obj = self._get_timeout(timeout)
|
| 399 |
+
timeout_obj.start_connect()
|
| 400 |
+
conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout)
|
| 401 |
+
|
| 402 |
+
# Trigger any extra validation we need to do.
|
| 403 |
+
try:
|
| 404 |
+
self._validate_conn(conn)
|
| 405 |
+
except (SocketTimeout, BaseSSLError) as e:
|
| 406 |
+
# Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.
|
| 407 |
+
self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)
|
| 408 |
+
raise
|
| 409 |
+
|
| 410 |
+
# conn.request() calls http.client.*.request, not the method in
|
| 411 |
+
# urllib3.request. It also calls makefile (recv) on the socket.
|
| 412 |
+
try:
|
| 413 |
+
if chunked:
|
| 414 |
+
conn.request_chunked(method, url, **httplib_request_kw)
|
| 415 |
+
else:
|
| 416 |
+
conn.request(method, url, **httplib_request_kw)
|
| 417 |
+
|
| 418 |
+
# We are swallowing BrokenPipeError (errno.EPIPE) since the server is
|
| 419 |
+
# legitimately able to close the connection after sending a valid response.
|
| 420 |
+
# With this behaviour, the received response is still readable.
|
| 421 |
+
except BrokenPipeError:
|
| 422 |
+
# Python 3
|
| 423 |
+
pass
|
| 424 |
+
except IOError as e:
|
| 425 |
+
# Python 2 and macOS/Linux
|
| 426 |
+
# EPIPE and ESHUTDOWN are BrokenPipeError on Python 2, and EPROTOTYPE/ECONNRESET are needed on macOS
|
| 427 |
+
# https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/
|
| 428 |
+
if e.errno not in {
|
| 429 |
+
errno.EPIPE,
|
| 430 |
+
errno.ESHUTDOWN,
|
| 431 |
+
errno.EPROTOTYPE,
|
| 432 |
+
errno.ECONNRESET,
|
| 433 |
+
}:
|
| 434 |
+
raise
|
| 435 |
+
|
| 436 |
+
# Reset the timeout for the recv() on the socket
|
| 437 |
+
read_timeout = timeout_obj.read_timeout
|
| 438 |
+
|
| 439 |
+
# App Engine doesn't have a sock attr
|
| 440 |
+
if getattr(conn, "sock", None):
|
| 441 |
+
# In Python 3 socket.py will catch EAGAIN and return None when you
|
| 442 |
+
# try and read into the file pointer created by http.client, which
|
| 443 |
+
# instead raises a BadStatusLine exception. Instead of catching
|
| 444 |
+
# the exception and assuming all BadStatusLine exceptions are read
|
| 445 |
+
# timeouts, check for a zero timeout before making the request.
|
| 446 |
+
if read_timeout == 0:
|
| 447 |
+
raise ReadTimeoutError(
|
| 448 |
+
self, url, "Read timed out. (read timeout=%s)" % read_timeout
|
| 449 |
+
)
|
| 450 |
+
if read_timeout is Timeout.DEFAULT_TIMEOUT:
|
| 451 |
+
conn.sock.settimeout(socket.getdefaulttimeout())
|
| 452 |
+
else: # None or a value
|
| 453 |
+
conn.sock.settimeout(read_timeout)
|
| 454 |
+
|
| 455 |
+
# Receive the response from the server
|
| 456 |
+
try:
|
| 457 |
+
try:
|
| 458 |
+
# Python 2.7, use buffering of HTTP responses
|
| 459 |
+
httplib_response = conn.getresponse(buffering=True)
|
| 460 |
+
except TypeError:
|
| 461 |
+
# Python 3
|
| 462 |
+
try:
|
| 463 |
+
httplib_response = conn.getresponse()
|
| 464 |
+
except BaseException as e:
|
| 465 |
+
# Remove the TypeError from the exception chain in
|
| 466 |
+
# Python 3 (including for exceptions like SystemExit).
|
| 467 |
+
# Otherwise it looks like a bug in the code.
|
| 468 |
+
six.raise_from(e, None)
|
| 469 |
+
except (SocketTimeout, BaseSSLError, SocketError) as e:
|
| 470 |
+
self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
|
| 471 |
+
raise
|
| 472 |
+
|
| 473 |
+
# AppEngine doesn't have a version attr.
|
| 474 |
+
http_version = getattr(conn, "_http_vsn_str", "HTTP/?")
|
| 475 |
+
log.debug(
|
| 476 |
+
'%s://%s:%s "%s %s %s" %s %s',
|
| 477 |
+
self.scheme,
|
| 478 |
+
self.host,
|
| 479 |
+
self.port,
|
| 480 |
+
method,
|
| 481 |
+
url,
|
| 482 |
+
http_version,
|
| 483 |
+
httplib_response.status,
|
| 484 |
+
httplib_response.length,
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
try:
|
| 488 |
+
assert_header_parsing(httplib_response.msg)
|
| 489 |
+
except (HeaderParsingError, TypeError) as hpe: # Platform-specific: Python 3
|
| 490 |
+
log.warning(
|
| 491 |
+
"Failed to parse headers (url=%s): %s",
|
| 492 |
+
self._absolute_url(url),
|
| 493 |
+
hpe,
|
| 494 |
+
exc_info=True,
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
return httplib_response
|
| 498 |
+
|
| 499 |
+
def _absolute_url(self, path):
|
| 500 |
+
return Url(scheme=self.scheme, host=self.host, port=self.port, path=path).url
|
| 501 |
+
|
| 502 |
+
def close(self):
|
| 503 |
+
"""
|
| 504 |
+
Close all pooled connections and disable the pool.
|
| 505 |
+
"""
|
| 506 |
+
if self.pool is None:
|
| 507 |
+
return
|
| 508 |
+
# Disable access to the pool
|
| 509 |
+
old_pool, self.pool = self.pool, None
|
| 510 |
+
|
| 511 |
+
# Close all the HTTPConnections in the pool.
|
| 512 |
+
_close_pool_connections(old_pool)
|
| 513 |
+
|
| 514 |
+
def is_same_host(self, url):
|
| 515 |
+
"""
|
| 516 |
+
Check if the given ``url`` is a member of the same host as this
|
| 517 |
+
connection pool.
|
| 518 |
+
"""
|
| 519 |
+
if url.startswith("/"):
|
| 520 |
+
return True
|
| 521 |
+
|
| 522 |
+
# TODO: Add optional support for socket.gethostbyname checking.
|
| 523 |
+
scheme, host, port = get_host(url)
|
| 524 |
+
if host is not None:
|
| 525 |
+
host = _normalize_host(host, scheme=scheme)
|
| 526 |
+
|
| 527 |
+
# Use explicit default port for comparison when none is given
|
| 528 |
+
if self.port and not port:
|
| 529 |
+
port = port_by_scheme.get(scheme)
|
| 530 |
+
elif not self.port and port == port_by_scheme.get(scheme):
|
| 531 |
+
port = None
|
| 532 |
+
|
| 533 |
+
return (scheme, host, port) == (self.scheme, self.host, self.port)
|
| 534 |
+
|
| 535 |
+
def urlopen(
|
| 536 |
+
self,
|
| 537 |
+
method,
|
| 538 |
+
url,
|
| 539 |
+
body=None,
|
| 540 |
+
headers=None,
|
| 541 |
+
retries=None,
|
| 542 |
+
redirect=True,
|
| 543 |
+
assert_same_host=True,
|
| 544 |
+
timeout=_Default,
|
| 545 |
+
pool_timeout=None,
|
| 546 |
+
release_conn=None,
|
| 547 |
+
chunked=False,
|
| 548 |
+
body_pos=None,
|
| 549 |
+
**response_kw
|
| 550 |
+
):
|
| 551 |
+
"""
|
| 552 |
+
Get a connection from the pool and perform an HTTP request. This is the
|
| 553 |
+
lowest level call for making a request, so you'll need to specify all
|
| 554 |
+
the raw details.
|
| 555 |
+
|
| 556 |
+
.. note::
|
| 557 |
+
|
| 558 |
+
More commonly, it's appropriate to use a convenience method provided
|
| 559 |
+
by :class:`.RequestMethods`, such as :meth:`request`.
|
| 560 |
+
|
| 561 |
+
.. note::
|
| 562 |
+
|
| 563 |
+
`release_conn` will only behave as expected if
|
| 564 |
+
`preload_content=False` because we want to make
|
| 565 |
+
`preload_content=False` the default behaviour someday soon without
|
| 566 |
+
breaking backwards compatibility.
|
| 567 |
+
|
| 568 |
+
:param method:
|
| 569 |
+
HTTP request method (such as GET, POST, PUT, etc.)
|
| 570 |
+
|
| 571 |
+
:param url:
|
| 572 |
+
The URL to perform the request on.
|
| 573 |
+
|
| 574 |
+
:param body:
|
| 575 |
+
Data to send in the request body, either :class:`str`, :class:`bytes`,
|
| 576 |
+
an iterable of :class:`str`/:class:`bytes`, or a file-like object.
|
| 577 |
+
|
| 578 |
+
:param headers:
|
| 579 |
+
Dictionary of custom headers to send, such as User-Agent,
|
| 580 |
+
If-None-Match, etc. If None, pool headers are used. If provided,
|
| 581 |
+
these headers completely replace any pool-specific headers.
|
| 582 |
+
|
| 583 |
+
:param retries:
|
| 584 |
+
Configure the number of retries to allow before raising a
|
| 585 |
+
:class:`~urllib3.exceptions.MaxRetryError` exception.
|
| 586 |
+
|
| 587 |
+
Pass ``None`` to retry until you receive a response. Pass a
|
| 588 |
+
:class:`~urllib3.util.retry.Retry` object for fine-grained control
|
| 589 |
+
over different types of retries.
|
| 590 |
+
Pass an integer number to retry connection errors that many times,
|
| 591 |
+
but no other types of errors. Pass zero to never retry.
|
| 592 |
+
|
| 593 |
+
If ``False``, then retries are disabled and any exception is raised
|
| 594 |
+
immediately. Also, instead of raising a MaxRetryError on redirects,
|
| 595 |
+
the redirect response will be returned.
|
| 596 |
+
|
| 597 |
+
:type retries: :class:`~urllib3.util.retry.Retry`, False, or an int.
|
| 598 |
+
|
| 599 |
+
:param redirect:
|
| 600 |
+
If True, automatically handle redirects (status codes 301, 302,
|
| 601 |
+
303, 307, 308). Each redirect counts as a retry. Disabling retries
|
| 602 |
+
will disable redirect, too.
|
| 603 |
+
|
| 604 |
+
:param assert_same_host:
|
| 605 |
+
If ``True``, will make sure that the host of the pool requests is
|
| 606 |
+
consistent else will raise HostChangedError. When ``False``, you can
|
| 607 |
+
use the pool on an HTTP proxy and request foreign hosts.
|
| 608 |
+
|
| 609 |
+
:param timeout:
|
| 610 |
+
If specified, overrides the default timeout for this one
|
| 611 |
+
request. It may be a float (in seconds) or an instance of
|
| 612 |
+
:class:`urllib3.util.Timeout`.
|
| 613 |
+
|
| 614 |
+
:param pool_timeout:
|
| 615 |
+
If set and the pool is set to block=True, then this method will
|
| 616 |
+
block for ``pool_timeout`` seconds and raise EmptyPoolError if no
|
| 617 |
+
connection is available within the time period.
|
| 618 |
+
|
| 619 |
+
:param release_conn:
|
| 620 |
+
If False, then the urlopen call will not release the connection
|
| 621 |
+
back into the pool once a response is received (but will release if
|
| 622 |
+
you read the entire contents of the response such as when
|
| 623 |
+
`preload_content=True`). This is useful if you're not preloading
|
| 624 |
+
the response's content immediately. You will need to call
|
| 625 |
+
``r.release_conn()`` on the response ``r`` to return the connection
|
| 626 |
+
back into the pool. If None, it takes the value of
|
| 627 |
+
``response_kw.get('preload_content', True)``.
|
| 628 |
+
|
| 629 |
+
:param chunked:
|
| 630 |
+
If True, urllib3 will send the body using chunked transfer
|
| 631 |
+
encoding. Otherwise, urllib3 will send the body using the standard
|
| 632 |
+
content-length form. Defaults to False.
|
| 633 |
+
|
| 634 |
+
:param int body_pos:
|
| 635 |
+
Position to seek to in file-like body in the event of a retry or
|
| 636 |
+
redirect. Typically this won't need to be set because urllib3 will
|
| 637 |
+
auto-populate the value when needed.
|
| 638 |
+
|
| 639 |
+
:param \\**response_kw:
|
| 640 |
+
Additional parameters are passed to
|
| 641 |
+
:meth:`urllib3.response.HTTPResponse.from_httplib`
|
| 642 |
+
"""
|
| 643 |
+
|
| 644 |
+
parsed_url = parse_url(url)
|
| 645 |
+
destination_scheme = parsed_url.scheme
|
| 646 |
+
|
| 647 |
+
if headers is None:
|
| 648 |
+
headers = self.headers
|
| 649 |
+
|
| 650 |
+
if not isinstance(retries, Retry):
|
| 651 |
+
retries = Retry.from_int(retries, redirect=redirect, default=self.retries)
|
| 652 |
+
|
| 653 |
+
if release_conn is None:
|
| 654 |
+
release_conn = response_kw.get("preload_content", True)
|
| 655 |
+
|
| 656 |
+
# Check host
|
| 657 |
+
if assert_same_host and not self.is_same_host(url):
|
| 658 |
+
raise HostChangedError(self, url, retries)
|
| 659 |
+
|
| 660 |
+
# Ensure that the URL we're connecting to is properly encoded
|
| 661 |
+
if url.startswith("/"):
|
| 662 |
+
url = six.ensure_str(_encode_target(url))
|
| 663 |
+
else:
|
| 664 |
+
url = six.ensure_str(parsed_url.url)
|
| 665 |
+
|
| 666 |
+
conn = None
|
| 667 |
+
|
| 668 |
+
# Track whether `conn` needs to be released before
|
| 669 |
+
# returning/raising/recursing. Update this variable if necessary, and
|
| 670 |
+
# leave `release_conn` constant throughout the function. That way, if
|
| 671 |
+
# the function recurses, the original value of `release_conn` will be
|
| 672 |
+
# passed down into the recursive call, and its value will be respected.
|
| 673 |
+
#
|
| 674 |
+
# See issue #651 [1] for details.
|
| 675 |
+
#
|
| 676 |
+
# [1] <https://github.com/urllib3/urllib3/issues/651>
|
| 677 |
+
release_this_conn = release_conn
|
| 678 |
+
|
| 679 |
+
http_tunnel_required = connection_requires_http_tunnel(
|
| 680 |
+
self.proxy, self.proxy_config, destination_scheme
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
# Merge the proxy headers. Only done when not using HTTP CONNECT. We
|
| 684 |
+
# have to copy the headers dict so we can safely change it without those
|
| 685 |
+
# changes being reflected in anyone else's copy.
|
| 686 |
+
if not http_tunnel_required:
|
| 687 |
+
headers = headers.copy()
|
| 688 |
+
headers.update(self.proxy_headers)
|
| 689 |
+
|
| 690 |
+
# Must keep the exception bound to a separate variable or else Python 3
|
| 691 |
+
# complains about UnboundLocalError.
|
| 692 |
+
err = None
|
| 693 |
+
|
| 694 |
+
# Keep track of whether we cleanly exited the except block. This
|
| 695 |
+
# ensures we do proper cleanup in finally.
|
| 696 |
+
clean_exit = False
|
| 697 |
+
|
| 698 |
+
# Rewind body position, if needed. Record current position
|
| 699 |
+
# for future rewinds in the event of a redirect/retry.
|
| 700 |
+
body_pos = set_file_position(body, body_pos)
|
| 701 |
+
|
| 702 |
+
try:
|
| 703 |
+
# Request a connection from the queue.
|
| 704 |
+
timeout_obj = self._get_timeout(timeout)
|
| 705 |
+
conn = self._get_conn(timeout=pool_timeout)
|
| 706 |
+
|
| 707 |
+
conn.timeout = timeout_obj.connect_timeout
|
| 708 |
+
|
| 709 |
+
is_new_proxy_conn = self.proxy is not None and not getattr(
|
| 710 |
+
conn, "sock", None
|
| 711 |
+
)
|
| 712 |
+
if is_new_proxy_conn and http_tunnel_required:
|
| 713 |
+
self._prepare_proxy(conn)
|
| 714 |
+
|
| 715 |
+
# Make the request on the httplib connection object.
|
| 716 |
+
httplib_response = self._make_request(
|
| 717 |
+
conn,
|
| 718 |
+
method,
|
| 719 |
+
url,
|
| 720 |
+
timeout=timeout_obj,
|
| 721 |
+
body=body,
|
| 722 |
+
headers=headers,
|
| 723 |
+
chunked=chunked,
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
# If we're going to release the connection in ``finally:``, then
|
| 727 |
+
# the response doesn't need to know about the connection. Otherwise
|
| 728 |
+
# it will also try to release it and we'll have a double-release
|
| 729 |
+
# mess.
|
| 730 |
+
response_conn = conn if not release_conn else None
|
| 731 |
+
|
| 732 |
+
# Pass method to Response for length checking
|
| 733 |
+
response_kw["request_method"] = method
|
| 734 |
+
|
| 735 |
+
# Import httplib's response into our own wrapper object
|
| 736 |
+
response = self.ResponseCls.from_httplib(
|
| 737 |
+
httplib_response,
|
| 738 |
+
pool=self,
|
| 739 |
+
connection=response_conn,
|
| 740 |
+
retries=retries,
|
| 741 |
+
**response_kw
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
# Everything went great!
|
| 745 |
+
clean_exit = True
|
| 746 |
+
|
| 747 |
+
except EmptyPoolError:
|
| 748 |
+
# Didn't get a connection from the pool, no need to clean up
|
| 749 |
+
clean_exit = True
|
| 750 |
+
release_this_conn = False
|
| 751 |
+
raise
|
| 752 |
+
|
| 753 |
+
except (
|
| 754 |
+
TimeoutError,
|
| 755 |
+
HTTPException,
|
| 756 |
+
SocketError,
|
| 757 |
+
ProtocolError,
|
| 758 |
+
BaseSSLError,
|
| 759 |
+
SSLError,
|
| 760 |
+
CertificateError,
|
| 761 |
+
) as e:
|
| 762 |
+
# Discard the connection for these exceptions. It will be
|
| 763 |
+
# replaced during the next _get_conn() call.
|
| 764 |
+
clean_exit = False
|
| 765 |
+
|
| 766 |
+
def _is_ssl_error_message_from_http_proxy(ssl_error):
|
| 767 |
+
# We're trying to detect the message 'WRONG_VERSION_NUMBER' but
|
| 768 |
+
# SSLErrors are kinda all over the place when it comes to the message,
|
| 769 |
+
# so we try to cover our bases here!
|
| 770 |
+
message = " ".join(re.split("[^a-z]", str(ssl_error).lower()))
|
| 771 |
+
return (
|
| 772 |
+
"wrong version number" in message
|
| 773 |
+
or "unknown protocol" in message
|
| 774 |
+
or "record layer failure" in message
|
| 775 |
+
)
|
| 776 |
+
|
| 777 |
+
# Try to detect a common user error with proxies which is to
|
| 778 |
+
# set an HTTP proxy to be HTTPS when it should be 'http://'
|
| 779 |
+
# (ie {'http': 'http://proxy', 'https': 'https://proxy'})
|
| 780 |
+
# Instead we add a nice error message and point to a URL.
|
| 781 |
+
if (
|
| 782 |
+
isinstance(e, BaseSSLError)
|
| 783 |
+
and self.proxy
|
| 784 |
+
and _is_ssl_error_message_from_http_proxy(e)
|
| 785 |
+
and conn.proxy
|
| 786 |
+
and conn.proxy.scheme == "https"
|
| 787 |
+
):
|
| 788 |
+
e = ProxyError(
|
| 789 |
+
"Your proxy appears to only use HTTP and not HTTPS, "
|
| 790 |
+
"try changing your proxy URL to be HTTP. See: "
|
| 791 |
+
"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html"
|
| 792 |
+
"#https-proxy-error-http-proxy",
|
| 793 |
+
SSLError(e),
|
| 794 |
+
)
|
| 795 |
+
elif isinstance(e, (BaseSSLError, CertificateError)):
|
| 796 |
+
e = SSLError(e)
|
| 797 |
+
elif isinstance(e, (SocketError, NewConnectionError)) and self.proxy:
|
| 798 |
+
e = ProxyError("Cannot connect to proxy.", e)
|
| 799 |
+
elif isinstance(e, (SocketError, HTTPException)):
|
| 800 |
+
e = ProtocolError("Connection aborted.", e)
|
| 801 |
+
|
| 802 |
+
retries = retries.increment(
|
| 803 |
+
method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
|
| 804 |
+
)
|
| 805 |
+
retries.sleep()
|
| 806 |
+
|
| 807 |
+
# Keep track of the error for the retry warning.
|
| 808 |
+
err = e
|
| 809 |
+
|
| 810 |
+
finally:
|
| 811 |
+
if not clean_exit:
|
| 812 |
+
# We hit some kind of exception, handled or otherwise. We need
|
| 813 |
+
# to throw the connection away unless explicitly told not to.
|
| 814 |
+
# Close the connection, set the variable to None, and make sure
|
| 815 |
+
# we put the None back in the pool to avoid leaking it.
|
| 816 |
+
conn = conn and conn.close()
|
| 817 |
+
release_this_conn = True
|
| 818 |
+
|
| 819 |
+
if release_this_conn:
|
| 820 |
+
# Put the connection back to be reused. If the connection is
|
| 821 |
+
# expired then it will be None, which will get replaced with a
|
| 822 |
+
# fresh connection during _get_conn.
|
| 823 |
+
self._put_conn(conn)
|
| 824 |
+
|
| 825 |
+
if not conn:
|
| 826 |
+
# Try again
|
| 827 |
+
log.warning(
|
| 828 |
+
"Retrying (%r) after connection broken by '%r': %s", retries, err, url
|
| 829 |
+
)
|
| 830 |
+
return self.urlopen(
|
| 831 |
+
method,
|
| 832 |
+
url,
|
| 833 |
+
body,
|
| 834 |
+
headers,
|
| 835 |
+
retries,
|
| 836 |
+
redirect,
|
| 837 |
+
assert_same_host,
|
| 838 |
+
timeout=timeout,
|
| 839 |
+
pool_timeout=pool_timeout,
|
| 840 |
+
release_conn=release_conn,
|
| 841 |
+
chunked=chunked,
|
| 842 |
+
body_pos=body_pos,
|
| 843 |
+
**response_kw
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
# Handle redirect?
|
| 847 |
+
redirect_location = redirect and response.get_redirect_location()
|
| 848 |
+
if redirect_location:
|
| 849 |
+
if response.status == 303:
|
| 850 |
+
# Change the method according to RFC 9110, Section 15.4.4.
|
| 851 |
+
method = "GET"
|
| 852 |
+
# And lose the body not to transfer anything sensitive.
|
| 853 |
+
body = None
|
| 854 |
+
headers = HTTPHeaderDict(headers)._prepare_for_method_change()
|
| 855 |
+
|
| 856 |
+
try:
|
| 857 |
+
retries = retries.increment(method, url, response=response, _pool=self)
|
| 858 |
+
except MaxRetryError:
|
| 859 |
+
if retries.raise_on_redirect:
|
| 860 |
+
response.drain_conn()
|
| 861 |
+
raise
|
| 862 |
+
return response
|
| 863 |
+
|
| 864 |
+
response.drain_conn()
|
| 865 |
+
retries.sleep_for_retry(response)
|
| 866 |
+
log.debug("Redirecting %s -> %s", url, redirect_location)
|
| 867 |
+
return self.urlopen(
|
| 868 |
+
method,
|
| 869 |
+
redirect_location,
|
| 870 |
+
body,
|
| 871 |
+
headers,
|
| 872 |
+
retries=retries,
|
| 873 |
+
redirect=redirect,
|
| 874 |
+
assert_same_host=assert_same_host,
|
| 875 |
+
timeout=timeout,
|
| 876 |
+
pool_timeout=pool_timeout,
|
| 877 |
+
release_conn=release_conn,
|
| 878 |
+
chunked=chunked,
|
| 879 |
+
body_pos=body_pos,
|
| 880 |
+
**response_kw
|
| 881 |
+
)
|
| 882 |
+
|
| 883 |
+
# Check if we should retry the HTTP response.
|
| 884 |
+
has_retry_after = bool(response.headers.get("Retry-After"))
|
| 885 |
+
if retries.is_retry(method, response.status, has_retry_after):
|
| 886 |
+
try:
|
| 887 |
+
retries = retries.increment(method, url, response=response, _pool=self)
|
| 888 |
+
except MaxRetryError:
|
| 889 |
+
if retries.raise_on_status:
|
| 890 |
+
response.drain_conn()
|
| 891 |
+
raise
|
| 892 |
+
return response
|
| 893 |
+
|
| 894 |
+
response.drain_conn()
|
| 895 |
+
retries.sleep(response)
|
| 896 |
+
log.debug("Retry: %s", url)
|
| 897 |
+
return self.urlopen(
|
| 898 |
+
method,
|
| 899 |
+
url,
|
| 900 |
+
body,
|
| 901 |
+
headers,
|
| 902 |
+
retries=retries,
|
| 903 |
+
redirect=redirect,
|
| 904 |
+
assert_same_host=assert_same_host,
|
| 905 |
+
timeout=timeout,
|
| 906 |
+
pool_timeout=pool_timeout,
|
| 907 |
+
release_conn=release_conn,
|
| 908 |
+
chunked=chunked,
|
| 909 |
+
body_pos=body_pos,
|
| 910 |
+
**response_kw
|
| 911 |
+
)
|
| 912 |
+
|
| 913 |
+
return response
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
class HTTPSConnectionPool(HTTPConnectionPool):
|
| 917 |
+
"""
|
| 918 |
+
Same as :class:`.HTTPConnectionPool`, but HTTPS.
|
| 919 |
+
|
| 920 |
+
:class:`.HTTPSConnection` uses one of ``assert_fingerprint``,
|
| 921 |
+
``assert_hostname`` and ``host`` in this order to verify connections.
|
| 922 |
+
If ``assert_hostname`` is False, no verification is done.
|
| 923 |
+
|
| 924 |
+
The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``,
|
| 925 |
+
``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl`
|
| 926 |
+
is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade
|
| 927 |
+
the connection socket into an SSL socket.
|
| 928 |
+
"""
|
| 929 |
+
|
| 930 |
+
scheme = "https"
|
| 931 |
+
ConnectionCls = HTTPSConnection
|
| 932 |
+
|
| 933 |
+
def __init__(
|
| 934 |
+
self,
|
| 935 |
+
host,
|
| 936 |
+
port=None,
|
| 937 |
+
strict=False,
|
| 938 |
+
timeout=Timeout.DEFAULT_TIMEOUT,
|
| 939 |
+
maxsize=1,
|
| 940 |
+
block=False,
|
| 941 |
+
headers=None,
|
| 942 |
+
retries=None,
|
| 943 |
+
_proxy=None,
|
| 944 |
+
_proxy_headers=None,
|
| 945 |
+
key_file=None,
|
| 946 |
+
cert_file=None,
|
| 947 |
+
cert_reqs=None,
|
| 948 |
+
key_password=None,
|
| 949 |
+
ca_certs=None,
|
| 950 |
+
ssl_version=None,
|
| 951 |
+
assert_hostname=None,
|
| 952 |
+
assert_fingerprint=None,
|
| 953 |
+
ca_cert_dir=None,
|
| 954 |
+
**conn_kw
|
| 955 |
+
):
|
| 956 |
+
|
| 957 |
+
HTTPConnectionPool.__init__(
|
| 958 |
+
self,
|
| 959 |
+
host,
|
| 960 |
+
port,
|
| 961 |
+
strict,
|
| 962 |
+
timeout,
|
| 963 |
+
maxsize,
|
| 964 |
+
block,
|
| 965 |
+
headers,
|
| 966 |
+
retries,
|
| 967 |
+
_proxy,
|
| 968 |
+
_proxy_headers,
|
| 969 |
+
**conn_kw
|
| 970 |
+
)
|
| 971 |
+
|
| 972 |
+
self.key_file = key_file
|
| 973 |
+
self.cert_file = cert_file
|
| 974 |
+
self.cert_reqs = cert_reqs
|
| 975 |
+
self.key_password = key_password
|
| 976 |
+
self.ca_certs = ca_certs
|
| 977 |
+
self.ca_cert_dir = ca_cert_dir
|
| 978 |
+
self.ssl_version = ssl_version
|
| 979 |
+
self.assert_hostname = assert_hostname
|
| 980 |
+
self.assert_fingerprint = assert_fingerprint
|
| 981 |
+
|
| 982 |
+
def _prepare_conn(self, conn):
|
| 983 |
+
"""
|
| 984 |
+
Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket`
|
| 985 |
+
and establish the tunnel if proxy is used.
|
| 986 |
+
"""
|
| 987 |
+
|
| 988 |
+
if isinstance(conn, VerifiedHTTPSConnection):
|
| 989 |
+
conn.set_cert(
|
| 990 |
+
key_file=self.key_file,
|
| 991 |
+
key_password=self.key_password,
|
| 992 |
+
cert_file=self.cert_file,
|
| 993 |
+
cert_reqs=self.cert_reqs,
|
| 994 |
+
ca_certs=self.ca_certs,
|
| 995 |
+
ca_cert_dir=self.ca_cert_dir,
|
| 996 |
+
assert_hostname=self.assert_hostname,
|
| 997 |
+
assert_fingerprint=self.assert_fingerprint,
|
| 998 |
+
)
|
| 999 |
+
conn.ssl_version = self.ssl_version
|
| 1000 |
+
return conn
|
| 1001 |
+
|
| 1002 |
+
def _prepare_proxy(self, conn):
|
| 1003 |
+
"""
|
| 1004 |
+
Establishes a tunnel connection through HTTP CONNECT.
|
| 1005 |
+
|
| 1006 |
+
Tunnel connection is established early because otherwise httplib would
|
| 1007 |
+
improperly set Host: header to proxy's IP:port.
|
| 1008 |
+
"""
|
| 1009 |
+
|
| 1010 |
+
conn.set_tunnel(self._proxy_host, self.port, self.proxy_headers)
|
| 1011 |
+
|
| 1012 |
+
if self.proxy.scheme == "https":
|
| 1013 |
+
conn.tls_in_tls_required = True
|
| 1014 |
+
|
| 1015 |
+
conn.connect()
|
| 1016 |
+
|
| 1017 |
+
def _new_conn(self):
|
| 1018 |
+
"""
|
| 1019 |
+
Return a fresh :class:`http.client.HTTPSConnection`.
|
| 1020 |
+
"""
|
| 1021 |
+
self.num_connections += 1
|
| 1022 |
+
log.debug(
|
| 1023 |
+
"Starting new HTTPS connection (%d): %s:%s",
|
| 1024 |
+
self.num_connections,
|
| 1025 |
+
self.host,
|
| 1026 |
+
self.port or "443",
|
| 1027 |
+
)
|
| 1028 |
+
|
| 1029 |
+
if not self.ConnectionCls or self.ConnectionCls is DummyConnection:
|
| 1030 |
+
raise SSLError(
|
| 1031 |
+
"Can't connect to HTTPS URL because the SSL module is not available."
|
| 1032 |
+
)
|
| 1033 |
+
|
| 1034 |
+
actual_host = self.host
|
| 1035 |
+
actual_port = self.port
|
| 1036 |
+
if self.proxy is not None:
|
| 1037 |
+
actual_host = self.proxy.host
|
| 1038 |
+
actual_port = self.proxy.port
|
| 1039 |
+
|
| 1040 |
+
conn = self.ConnectionCls(
|
| 1041 |
+
host=actual_host,
|
| 1042 |
+
port=actual_port,
|
| 1043 |
+
timeout=self.timeout.connect_timeout,
|
| 1044 |
+
strict=self.strict,
|
| 1045 |
+
cert_file=self.cert_file,
|
| 1046 |
+
key_file=self.key_file,
|
| 1047 |
+
key_password=self.key_password,
|
| 1048 |
+
**self.conn_kw
|
| 1049 |
+
)
|
| 1050 |
+
|
| 1051 |
+
return self._prepare_conn(conn)
|
| 1052 |
+
|
| 1053 |
+
def _validate_conn(self, conn):
|
| 1054 |
+
"""
|
| 1055 |
+
Called right before a request is made, after the socket is created.
|
| 1056 |
+
"""
|
| 1057 |
+
super(HTTPSConnectionPool, self)._validate_conn(conn)
|
| 1058 |
+
|
| 1059 |
+
# Force connect early to allow us to validate the connection.
|
| 1060 |
+
if not getattr(conn, "sock", None): # AppEngine might not have `.sock`
|
| 1061 |
+
conn.connect()
|
| 1062 |
+
|
| 1063 |
+
if not conn.is_verified:
|
| 1064 |
+
warnings.warn(
|
| 1065 |
+
(
|
| 1066 |
+
"Unverified HTTPS request is being made to host '%s'. "
|
| 1067 |
+
"Adding certificate verification is strongly advised. See: "
|
| 1068 |
+
"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html"
|
| 1069 |
+
"#ssl-warnings" % conn.host
|
| 1070 |
+
),
|
| 1071 |
+
InsecureRequestWarning,
|
| 1072 |
+
)
|
| 1073 |
+
|
| 1074 |
+
if getattr(conn, "proxy_is_verified", None) is False:
|
| 1075 |
+
warnings.warn(
|
| 1076 |
+
(
|
| 1077 |
+
"Unverified HTTPS connection done to an HTTPS proxy. "
|
| 1078 |
+
"Adding certificate verification is strongly advised. See: "
|
| 1079 |
+
"https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html"
|
| 1080 |
+
"#ssl-warnings"
|
| 1081 |
+
),
|
| 1082 |
+
InsecureRequestWarning,
|
| 1083 |
+
)
|
| 1084 |
+
|
| 1085 |
+
|
| 1086 |
+
def connection_from_url(url, **kw):
|
| 1087 |
+
"""
|
| 1088 |
+
Given a url, return an :class:`.ConnectionPool` instance of its host.
|
| 1089 |
+
|
| 1090 |
+
This is a shortcut for not having to parse out the scheme, host, and port
|
| 1091 |
+
of the url before creating an :class:`.ConnectionPool` instance.
|
| 1092 |
+
|
| 1093 |
+
:param url:
|
| 1094 |
+
Absolute URL string that must include the scheme. Port is optional.
|
| 1095 |
+
|
| 1096 |
+
:param \\**kw:
|
| 1097 |
+
Passes additional parameters to the constructor of the appropriate
|
| 1098 |
+
:class:`.ConnectionPool`. Useful for specifying things like
|
| 1099 |
+
timeout, maxsize, headers, etc.
|
| 1100 |
+
|
| 1101 |
+
Example::
|
| 1102 |
+
|
| 1103 |
+
>>> conn = connection_from_url('http://google.com/')
|
| 1104 |
+
>>> r = conn.request('GET', '/')
|
| 1105 |
+
"""
|
| 1106 |
+
scheme, host, port = get_host(url)
|
| 1107 |
+
port = port or port_by_scheme.get(scheme, 80)
|
| 1108 |
+
if scheme == "https":
|
| 1109 |
+
return HTTPSConnectionPool(host, port=port, **kw)
|
| 1110 |
+
else:
|
| 1111 |
+
return HTTPConnectionPool(host, port=port, **kw)
|
| 1112 |
+
|
| 1113 |
+
|
| 1114 |
+
def _normalize_host(host, scheme):
|
| 1115 |
+
"""
|
| 1116 |
+
Normalize hosts for comparisons and use with sockets.
|
| 1117 |
+
"""
|
| 1118 |
+
|
| 1119 |
+
host = normalize_host(host, scheme)
|
| 1120 |
+
|
| 1121 |
+
# httplib doesn't like it when we include brackets in IPv6 addresses
|
| 1122 |
+
# Specifically, if we include brackets but also pass the port then
|
| 1123 |
+
# httplib crazily doubles up the square brackets on the Host header.
|
| 1124 |
+
# Instead, we need to make sure we never pass ``None`` as the port.
|
| 1125 |
+
# However, for backward compatibility reasons we can't actually
|
| 1126 |
+
# *assert* that. See http://bugs.python.org/issue28539
|
| 1127 |
+
if host.startswith("[") and host.endswith("]"):
|
| 1128 |
+
host = host[1:-1]
|
| 1129 |
+
return host
|
| 1130 |
+
|
| 1131 |
+
|
| 1132 |
+
def _close_pool_connections(pool):
|
| 1133 |
+
"""Drains a queue of connections and closes each one."""
|
| 1134 |
+
try:
|
| 1135 |
+
while True:
|
| 1136 |
+
conn = pool.get(block=False)
|
| 1137 |
+
if conn:
|
| 1138 |
+
conn.close()
|
| 1139 |
+
except queue.Empty:
|
| 1140 |
+
pass # Done.
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (206 Bytes). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/_appengine_environ.cpython-312.pyc
ADDED
|
Binary file (1.86 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/appengine.cpython-312.pyc
ADDED
|
Binary file (11.6 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/ntlmpool.cpython-312.pyc
ADDED
|
Binary file (5.72 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/pyopenssl.cpython-312.pyc
ADDED
|
Binary file (24.5 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/securetransport.cpython-312.pyc
ADDED
|
Binary file (35.5 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__pycache__/socks.cpython-312.pyc
ADDED
|
Binary file (7.52 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This module provides means to detect the App Engine environment.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def is_appengine():
|
| 9 |
+
return is_local_appengine() or is_prod_appengine()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def is_appengine_sandbox():
|
| 13 |
+
"""Reports if the app is running in the first generation sandbox.
|
| 14 |
+
|
| 15 |
+
The second generation runtimes are technically still in a sandbox, but it
|
| 16 |
+
is much less restrictive, so generally you shouldn't need to check for it.
|
| 17 |
+
see https://cloud.google.com/appengine/docs/standard/runtimes
|
| 18 |
+
"""
|
| 19 |
+
return is_appengine() and os.environ["APPENGINE_RUNTIME"] == "python27"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def is_local_appengine():
|
| 23 |
+
return "APPENGINE_RUNTIME" in os.environ and os.environ.get(
|
| 24 |
+
"SERVER_SOFTWARE", ""
|
| 25 |
+
).startswith("Development/")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def is_prod_appengine():
|
| 29 |
+
return "APPENGINE_RUNTIME" in os.environ and os.environ.get(
|
| 30 |
+
"SERVER_SOFTWARE", ""
|
| 31 |
+
).startswith("Google App Engine/")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def is_prod_appengine_mvms():
|
| 35 |
+
"""Deprecated."""
|
| 36 |
+
return False
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (223 Bytes). View file
|
|
|
lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__pycache__/bindings.cpython-312.pyc
ADDED
|
Binary file (17.4 kB). View file
|
|
|