Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f65547e6e8 |
18
.pre-commit-config.yaml
Normal file
18
.pre-commit-config.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.0
|
||||
hooks:
|
||||
- id: ruff # linting
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black # formatting
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from black import patched_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(patched_main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from blackd import patched_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(patched_main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from identify.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from nodeenv import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(main())
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(main())
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(main())
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from pre_commit.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(console_main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from pygments.cmdline import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(console_main())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/Users/emoniefaychetwin/PPE2/PPE2/.venv/bin/python
|
||||
import sys
|
||||
from virtualenv.__main__ import run_with_catch
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = sys.argv[0].removesuffix(".exe")
|
||||
sys.exit(run_with_catch())
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__all__ = ["__version__", "version_tuple"]
|
||||
|
||||
try:
|
||||
|
||||
@ -12,7 +12,6 @@ from .code import TracebackEntry
|
||||
from .source import getrawcode
|
||||
from .source import Source
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Code",
|
||||
"ExceptionInfo",
|
||||
|
||||
@ -48,7 +48,6 @@ from _pytest.deprecated import check_ispytest
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
@ -230,6 +229,7 @@ class TracebackEntry:
|
||||
@property
|
||||
def end_colno(self) -> int | None:
|
||||
return None
|
||||
|
||||
else:
|
||||
|
||||
@property
|
||||
@ -595,33 +595,33 @@ class ExceptionInfo(Generic[E]):
|
||||
@property
|
||||
def type(self) -> type[E]:
|
||||
"""The exception class."""
|
||||
assert self._excinfo is not None, (
|
||||
".type can only be used after the context manager exits"
|
||||
)
|
||||
assert (
|
||||
self._excinfo is not None
|
||||
), ".type can only be used after the context manager exits"
|
||||
return self._excinfo[0]
|
||||
|
||||
@property
|
||||
def value(self) -> E:
|
||||
"""The exception value."""
|
||||
assert self._excinfo is not None, (
|
||||
".value can only be used after the context manager exits"
|
||||
)
|
||||
assert (
|
||||
self._excinfo is not None
|
||||
), ".value can only be used after the context manager exits"
|
||||
return self._excinfo[1]
|
||||
|
||||
@property
|
||||
def tb(self) -> TracebackType:
|
||||
"""The exception raw traceback."""
|
||||
assert self._excinfo is not None, (
|
||||
".tb can only be used after the context manager exits"
|
||||
)
|
||||
assert (
|
||||
self._excinfo is not None
|
||||
), ".tb can only be used after the context manager exits"
|
||||
return self._excinfo[2]
|
||||
|
||||
@property
|
||||
def typename(self) -> str:
|
||||
"""The type name of the exception."""
|
||||
assert self._excinfo is not None, (
|
||||
".typename can only be used after the context manager exits"
|
||||
)
|
||||
assert (
|
||||
self._excinfo is not None
|
||||
), ".typename can only be used after the context manager exits"
|
||||
return self.type.__name__
|
||||
|
||||
@property
|
||||
|
||||
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from .terminalwriter import get_terminal_width
|
||||
from .terminalwriter import TerminalWriter
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TerminalWriter",
|
||||
"get_terminal_width",
|
||||
|
||||
@ -113,7 +113,7 @@ class PrettyPrinter:
|
||||
elif (
|
||||
_dataclasses.is_dataclass(object)
|
||||
and not isinstance(object, type)
|
||||
and object.__dataclass_params__.repr # type:ignore[attr-defined]
|
||||
and object.__dataclass_params__.repr # type: ignore[attr-defined]
|
||||
and
|
||||
# Check dataclass has generated repr method.
|
||||
hasattr(object.__repr__, "__wrapped__")
|
||||
|
||||
@ -19,7 +19,6 @@ from pygments.lexers.python import PythonLexer
|
||||
from ..compat import assert_never
|
||||
from .wcwidth import wcswidth
|
||||
|
||||
|
||||
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
|
||||
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ import sys
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
@ -94,7 +93,7 @@ class ErrorMaker:
|
||||
try:
|
||||
# error: Invalid index type "Optional[int]" for "dict[int, int]"; expected type "int" [index]
|
||||
# OK to ignore because we catch the KeyError below.
|
||||
cls = self._geterrnoclass(_winerrnomap[value.errno]) # type:ignore[index]
|
||||
cls = self._geterrnoclass(_winerrnomap[value.errno]) # type: ignore[index]
|
||||
except KeyError:
|
||||
raise value
|
||||
else:
|
||||
|
||||
@ -33,7 +33,6 @@ import warnings
|
||||
|
||||
from . import error
|
||||
|
||||
|
||||
# Moved from local.py.
|
||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||
|
||||
@ -222,7 +221,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import pwd
|
||||
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type: ignore[attr-defined,unused-ignore]
|
||||
return entry[0]
|
||||
|
||||
@property
|
||||
@ -232,7 +231,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import grp
|
||||
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type: ignore[attr-defined,unused-ignore]
|
||||
return entry[0]
|
||||
|
||||
def isdir(self):
|
||||
@ -250,7 +249,7 @@ def getuserid(user):
|
||||
import pwd
|
||||
|
||||
if not isinstance(user, int):
|
||||
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
|
||||
user = pwd.getpwnam(user)[2] # type: ignore[attr-defined,unused-ignore]
|
||||
return user
|
||||
|
||||
|
||||
@ -258,7 +257,7 @@ def getgroupid(group):
|
||||
import grp
|
||||
|
||||
if not isinstance(group, int):
|
||||
group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
|
||||
group = grp.getgrnam(group)[2] # type: ignore[attr-defined,unused-ignore]
|
||||
return group
|
||||
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
||||
commit_id: COMMIT_ID
|
||||
__commit_id__: COMMIT_ID
|
||||
|
||||
__version__ = version = '9.0.2'
|
||||
__version__ = version = "9.0.2"
|
||||
__version_tuple__ = version_tuple = (9, 0, 2)
|
||||
|
||||
__commit_id__ = commit_id = None
|
||||
|
||||
@ -18,7 +18,6 @@ from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.nodes import Item
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.main import Session
|
||||
|
||||
|
||||
@ -26,7 +26,6 @@ import types
|
||||
from typing import IO
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from importlib.resources.abc import TraversableResources
|
||||
else:
|
||||
@ -704,9 +703,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
pos = 0
|
||||
for item in mod.body:
|
||||
match item:
|
||||
case ast.Expr(value=ast.Constant(value=str() as doc)) if (
|
||||
expect_docstring
|
||||
):
|
||||
case ast.Expr(
|
||||
value=ast.Constant(value=str() as doc)
|
||||
) if expect_docstring:
|
||||
if self.is_rewrite_disabled(doc):
|
||||
return
|
||||
expect_docstring = False
|
||||
@ -1018,9 +1017,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
e.id for e in boolop.values[:i] if hasattr(e, "id")
|
||||
]:
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[self.scope][target_id] = v.left # type:ignore[assignment]
|
||||
self.variables_overwrite[self.scope][target_id] = v.left # type: ignore[assignment]
|
||||
# mypy's false positive, we're checking that the 'target' attribute exists.
|
||||
v.left.target.id = pytest_temp # type:ignore[attr-defined]
|
||||
v.left.target.id = pytest_temp # type: ignore[attr-defined]
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||
@ -1065,7 +1064,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
|
||||
arg = self.variables_overwrite[self.scope][arg.id] # type: ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
@ -1074,7 +1073,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
case ast.Name(id=id) if id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
keyword.value = self.variables_overwrite[self.scope][id] # type:ignore[assignment]
|
||||
keyword.value = self.variables_overwrite[self.scope][id] # type: ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
@ -1132,7 +1131,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
case (
|
||||
ast.NamedExpr(target=ast.Name(id=target_id)),
|
||||
ast.Name(id=name_id),
|
||||
) if target_id == name_id:
|
||||
) if (
|
||||
target_id == name_id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[self.scope][name_id] = next_operand # type: ignore[assignment]
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ from _pytest.compat import running_on_ci
|
||||
from _pytest.config import Config
|
||||
from _pytest.nodes import Item
|
||||
|
||||
|
||||
DEFAULT_MAX_LINES = 8
|
||||
DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80
|
||||
USAGE_MSG = "use '-vv' to show"
|
||||
|
||||
@ -23,7 +23,6 @@ from _pytest._io.saferepr import saferepr_unlimited
|
||||
from _pytest.compat import running_on_ci
|
||||
from _pytest.config import Config
|
||||
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
# loaded and in turn call the hooks defined here as part of the
|
||||
|
||||
@ -32,7 +32,6 @@ from _pytest.nodes import Directory
|
||||
from _pytest.nodes import File
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
README_CONTENT = """\
|
||||
# pytest cache directory #
|
||||
|
||||
|
||||
@ -27,7 +27,6 @@ from typing import NamedTuple
|
||||
from typing import TextIO
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
@ -42,7 +41,6 @@ from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
|
||||
|
||||
@ -393,11 +391,11 @@ class SysCaptureBase(CaptureBase[AnyStr]):
|
||||
)
|
||||
|
||||
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
|
||||
assert self._state in states, (
|
||||
"cannot {} in state {!r}: expected one of {}".format(
|
||||
assert (
|
||||
self._state in states
|
||||
), "cannot {} in state {!r}: expected one of {}".format(
|
||||
op, self._state, ", ".join(states)
|
||||
)
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
self._assert_state("start", ("initialized",))
|
||||
@ -510,11 +508,11 @@ class FDCaptureBase(CaptureBase[AnyStr]):
|
||||
)
|
||||
|
||||
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
|
||||
assert self._state in states, (
|
||||
"cannot {} in state {!r}: expected one of {}".format(
|
||||
assert (
|
||||
self._state in states
|
||||
), "cannot {} in state {!r}: expected one of {}".format(
|
||||
op, self._state, ", ".join(states)
|
||||
)
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start capturing on targetfd using memorized tmpfile."""
|
||||
|
||||
@ -18,7 +18,6 @@ from typing import NoReturn
|
||||
|
||||
import py
|
||||
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
from annotationlib import Format
|
||||
|
||||
|
||||
@ -74,7 +74,6 @@ from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
from _pytest.cacheprovider import Cache
|
||||
@ -344,7 +343,7 @@ def _prepareconfig(
|
||||
if isinstance(args, os.PathLike):
|
||||
args = [os.fspath(args)]
|
||||
elif not isinstance(args, list):
|
||||
msg = ( # type:ignore[unreachable]
|
||||
msg = ( # type: ignore[unreachable]
|
||||
"`args` parameter expected to be a list of strings, got: {!r} (type: {})"
|
||||
)
|
||||
raise TypeError(msg.format(args, type(args)))
|
||||
@ -861,9 +860,9 @@ class PytestPluginManager(PluginManager):
|
||||
# "terminal" or "capture". Those plugins are registered under their
|
||||
# basename for historic purposes but must be imported with the
|
||||
# _pytest prefix.
|
||||
assert isinstance(modname, str), (
|
||||
f"module name as text required, got {modname!r}"
|
||||
)
|
||||
assert isinstance(
|
||||
modname, str
|
||||
), f"module name as text required, got {modname!r}"
|
||||
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
|
||||
return
|
||||
|
||||
@ -1475,9 +1474,9 @@ class Config:
|
||||
|
||||
def parse(self, args: list[str], addopts: bool = True) -> None:
|
||||
# Parse given cmdline arguments into this config object.
|
||||
assert self.args == [], (
|
||||
"can only parse cmdline args at most once per Config object"
|
||||
)
|
||||
assert (
|
||||
self.args == []
|
||||
), "can only parse cmdline args at most once per Config object"
|
||||
|
||||
self.hook.pytest_addhooks.call_historic(
|
||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||
@ -2082,8 +2081,7 @@ def parse_warning_filter(
|
||||
* Raises UsageError so we get nice error messages on failure.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
error_template = dedent(
|
||||
f"""\
|
||||
error_template = dedent(f"""\
|
||||
while parsing the following warning configuration:
|
||||
|
||||
{arg}
|
||||
@ -2091,23 +2089,20 @@ def parse_warning_filter(
|
||||
This error occurred:
|
||||
|
||||
{{error}}
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
parts = arg.split(":")
|
||||
if len(parts) > 5:
|
||||
doc_url = (
|
||||
"https://docs.python.org/3/library/warnings.html#describing-warning-filters"
|
||||
)
|
||||
error = dedent(
|
||||
f"""\
|
||||
error = dedent(f"""\
|
||||
Too many fields ({len(parts)}), expected at most 5 separated by colons:
|
||||
|
||||
action:message:category:module:line
|
||||
|
||||
For more information please consult: {doc_url}
|
||||
"""
|
||||
)
|
||||
""")
|
||||
raise UsageError(error_template.format(error=error))
|
||||
|
||||
while len(parts) < 5:
|
||||
|
||||
@ -16,7 +16,6 @@ from .exceptions import UsageError
|
||||
import _pytest._io
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
@ -186,10 +185,19 @@ class Parser:
|
||||
self,
|
||||
name: str,
|
||||
help: str,
|
||||
type: Literal[
|
||||
"string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
|
||||
type: (
|
||||
Literal[
|
||||
"string",
|
||||
"paths",
|
||||
"pathlist",
|
||||
"args",
|
||||
"linelist",
|
||||
"bool",
|
||||
"int",
|
||||
"float",
|
||||
]
|
||||
| None = None,
|
||||
| None
|
||||
) = None,
|
||||
default: Any = NOT_SET,
|
||||
*,
|
||||
aliases: Sequence[str] = (),
|
||||
|
||||
@ -12,7 +12,6 @@ from ..compat import LEGACY_PATH
|
||||
from ..compat import legacy_path
|
||||
from ..deprecated import HOOK_LEGACY_PATH_ARG
|
||||
|
||||
|
||||
# hookname: (Path, LEGACY_PATH)
|
||||
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
|
||||
"pytest_ignore_collect": ("collection_path", "path"),
|
||||
|
||||
@ -18,7 +18,6 @@ from _pytest.warning_types import PytestRemovedIn9Warning
|
||||
from _pytest.warning_types import PytestRemovedIn10Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
|
||||
# set of plugins which have been integrated into the core; we use this list to ignore
|
||||
# them during registration to avoid conflicts
|
||||
DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
|
||||
@ -41,7 +41,6 @@ from _pytest.python import Module
|
||||
from _pytest.python_api import approx
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import doctest
|
||||
|
||||
@ -525,7 +524,7 @@ class DoctestModule(Module):
|
||||
obj = inspect.unwrap(obj)
|
||||
|
||||
# Type ignored because this is a private function.
|
||||
return super()._find_lineno( # type:ignore[misc]
|
||||
return super()._find_lineno( # type: ignore[misc]
|
||||
obj,
|
||||
source_lines,
|
||||
)
|
||||
|
||||
@ -10,7 +10,6 @@ from _pytest.nodes import Item
|
||||
from _pytest.stash import StashKey
|
||||
import pytest
|
||||
|
||||
|
||||
fault_handler_original_stderr_fd_key = StashKey[int]()
|
||||
fault_handler_stderr_fd_key = StashKey[int]()
|
||||
|
||||
|
||||
@ -70,7 +70,6 @@ from _pytest.scope import Scope
|
||||
from _pytest.warning_types import PytestRemovedIn9Warning
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
@ -759,9 +758,9 @@ class SubRequest(FixtureRequest):
|
||||
if node is None and scope is Scope.Class:
|
||||
# Fallback to function item itself.
|
||||
node = self._pyfuncitem
|
||||
assert node, (
|
||||
f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
|
||||
)
|
||||
assert (
|
||||
node
|
||||
), f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
|
||||
return node
|
||||
|
||||
def _check_scope(
|
||||
@ -821,9 +820,9 @@ class FixtureLookupError(LookupError):
|
||||
# new cases it might break.
|
||||
# Add the assert to make it clearer to developer that this will fail, otherwise
|
||||
# it crashes because `fspath` does not get set due to `stack` being empty.
|
||||
assert self.msg is None or self.fixturestack, (
|
||||
"formatrepr assumptions broken, rewrite it to handle it"
|
||||
)
|
||||
assert (
|
||||
self.msg is None or self.fixturestack
|
||||
), "formatrepr assumptions broken, rewrite it to handle it"
|
||||
if msg is not None:
|
||||
# The last fixture raise an error, let's present
|
||||
# it at the requesting side.
|
||||
@ -1950,9 +1949,11 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
||||
if fixture_doc:
|
||||
write_docstring(
|
||||
tw,
|
||||
(
|
||||
fixture_doc.split("\n\n", maxsplit=1)[0]
|
||||
if verbose <= 0
|
||||
else fixture_doc,
|
||||
else fixture_doc
|
||||
),
|
||||
)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
|
||||
@ -15,7 +15,6 @@ from pluggy import HookspecMarker
|
||||
|
||||
from .deprecated import HOOK_LEGACY_PATH_ARG
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pdb
|
||||
from typing import Literal
|
||||
@ -1043,7 +1042,7 @@ def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None
|
||||
),
|
||||
},
|
||||
)
|
||||
def pytest_report_header( # type:ignore[empty-body]
|
||||
def pytest_report_header( # type: ignore[empty-body]
|
||||
config: Config, start_path: Path, startdir: LEGACY_PATH
|
||||
) -> str | list[str]:
|
||||
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
@ -1079,7 +1078,7 @@ def pytest_report_header( # type:ignore[empty-body]
|
||||
),
|
||||
},
|
||||
)
|
||||
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
def pytest_report_collectionfinish( # type: ignore[empty-body]
|
||||
config: Config,
|
||||
start_path: Path,
|
||||
startdir: LEGACY_PATH,
|
||||
@ -1118,7 +1117,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||
def pytest_report_teststatus( # type: ignore[empty-body]
|
||||
report: CollectReport | TestReport, config: Config
|
||||
) -> TestShortLogReport | tuple[str, str, str | tuple[str, Mapping[str, bool]]]:
|
||||
"""Return result-category, shortletter and verbose word for status
|
||||
@ -1216,7 +1215,7 @@ def pytest_warning_recorded(
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_markeval_namespace( # type:ignore[empty-body]
|
||||
def pytest_markeval_namespace( # type: ignore[empty-body]
|
||||
config: Config,
|
||||
) -> dict[str, Any]:
|
||||
"""Called when constructing the globals dictionary used for
|
||||
|
||||
@ -30,7 +30,6 @@ from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
import pytest
|
||||
|
||||
|
||||
xml_key = StashKey["LogXML"]()
|
||||
|
||||
|
||||
|
||||
@ -33,7 +33,6 @@ from _pytest.pytester import RunResult
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pexpect
|
||||
|
||||
|
||||
@ -41,7 +41,6 @@ from _pytest.main import Session
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
logging_StreamHandler = logging.StreamHandler[StringIO]
|
||||
else:
|
||||
|
||||
@ -48,7 +48,6 @@ from _pytest.runner import collect_one_node
|
||||
from _pytest.runner import SetupState
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
@ -27,7 +27,6 @@ from _pytest.config.argparsing import NOT_SET
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.nodes import Item
|
||||
|
||||
|
||||
@ -38,7 +38,6 @@ from typing import NoReturn
|
||||
from typing import overload
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Expression",
|
||||
"ExpressionMatcher",
|
||||
|
||||
@ -31,7 +31,6 @@ from _pytest.raises import AbstractRaises
|
||||
from _pytest.scope import _ScopeName
|
||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..nodes import Node
|
||||
|
||||
@ -500,10 +499,12 @@ if TYPE_CHECKING:
|
||||
*conditions: str | bool,
|
||||
reason: str = ...,
|
||||
run: bool = ...,
|
||||
raises: None
|
||||
raises: (
|
||||
None
|
||||
| type[BaseException]
|
||||
| tuple[type[BaseException], ...]
|
||||
| AbstractRaises[BaseException] = ...,
|
||||
| AbstractRaises[BaseException]
|
||||
) = ...,
|
||||
strict: bool = ...,
|
||||
) -> MarkDecorator: ...
|
||||
|
||||
@ -514,9 +515,11 @@ if TYPE_CHECKING:
|
||||
argvalues: Iterable[ParameterSet | Sequence[object] | object],
|
||||
*,
|
||||
indirect: bool | Sequence[str] = ...,
|
||||
ids: Iterable[None | str | float | int | bool]
|
||||
ids: (
|
||||
Iterable[None | str | float | int | bool]
|
||||
| Callable[[Any], object | None]
|
||||
| None = ...,
|
||||
| None
|
||||
) = ...,
|
||||
scope: _ScopeName | None = ...,
|
||||
) -> MarkDecorator: ...
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ from _pytest.deprecated import MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
|
||||
|
||||
|
||||
|
||||
@ -41,7 +41,6 @@ from _pytest.pathlib import absolutepath
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
import pytest
|
||||
|
||||
|
||||
pastebinfile_key = StashKey[IO[bytes]]()
|
||||
|
||||
|
||||
|
||||
@ -37,7 +37,6 @@ from _pytest.compat import assert_never
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader
|
||||
else:
|
||||
@ -73,8 +72,10 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||
def on_rm_rf_error(
|
||||
func: Callable[..., Any] | None,
|
||||
path: str,
|
||||
excinfo: BaseException
|
||||
| tuple[type[BaseException], BaseException, types.TracebackType | None],
|
||||
excinfo: (
|
||||
BaseException
|
||||
| tuple[type[BaseException], BaseException, types.TracebackType | None]
|
||||
),
|
||||
*,
|
||||
start_path: Path,
|
||||
) -> bool:
|
||||
|
||||
@ -67,7 +67,6 @@ from _pytest.reports import TestReport
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
from _pytest.warning_types import PytestFDWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pexpect
|
||||
|
||||
|
||||
@ -77,7 +77,6 @@ from _pytest.stash import StashKey
|
||||
from _pytest.warning_types import PytestCollectionWarning
|
||||
from _pytest.warning_types import PytestReturnNotNoneWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
@ -959,9 +958,9 @@ class IdMaker:
|
||||
new_id = f"{id}{suffix}{id_suffixes[id]}"
|
||||
resolved_ids[index] = new_id
|
||||
id_suffixes[id] += 1
|
||||
assert len(resolved_ids) == len(set(resolved_ids)), (
|
||||
f"Internal error: {resolved_ids=}"
|
||||
)
|
||||
assert len(resolved_ids) == len(
|
||||
set(resolved_ids)
|
||||
), f"Internal error: {resolved_ids=}"
|
||||
return resolved_ids
|
||||
|
||||
def _strict_parametrization_ids_enabled(self) -> bool:
|
||||
|
||||
@ -13,7 +13,6 @@ import sys
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from numpy import ndarray
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ from _pytest._code.code import stringify_exception
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Sequence
|
||||
@ -709,9 +708,9 @@ class RaisesExc(AbstractRaises[BaseExcT_co_default]):
|
||||
|
||||
fail(f"DID NOT RAISE {self.expected_exceptions[0]!r}")
|
||||
|
||||
assert self.excinfo is not None, (
|
||||
"Internal error - should have been constructed in __enter__"
|
||||
)
|
||||
assert (
|
||||
self.excinfo is not None
|
||||
), "Internal error - should have been constructed in __enter__"
|
||||
|
||||
if not self.matches(exc_val):
|
||||
if self._just_propagate:
|
||||
@ -928,9 +927,9 @@ class RaisesGroup(AbstractRaises[BaseExceptionGroup[BaseExcT_co]]):
|
||||
@overload
|
||||
def __init__(
|
||||
self: RaisesGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
|
||||
expected_exception: type[BaseExcT_1]
|
||||
| RaisesExc[BaseExcT_1]
|
||||
| RaisesGroup[BaseExcT_2],
|
||||
expected_exception: (
|
||||
type[BaseExcT_1] | RaisesExc[BaseExcT_1] | RaisesGroup[BaseExcT_2]
|
||||
),
|
||||
/,
|
||||
*other_exceptions: type[BaseExcT_1]
|
||||
| RaisesExc[BaseExcT_1]
|
||||
@ -947,9 +946,9 @@ class RaisesGroup(AbstractRaises[BaseExceptionGroup[BaseExcT_co]]):
|
||||
|
||||
def __init__(
|
||||
self: RaisesGroup[ExcT_1 | BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
|
||||
expected_exception: type[BaseExcT_1]
|
||||
| RaisesExc[BaseExcT_1]
|
||||
| RaisesGroup[BaseExcT_2],
|
||||
expected_exception: (
|
||||
type[BaseExcT_1] | RaisesExc[BaseExcT_1] | RaisesGroup[BaseExcT_2]
|
||||
),
|
||||
/,
|
||||
*other_exceptions: type[BaseExcT_1]
|
||||
| RaisesExc[BaseExcT_1]
|
||||
@ -1416,9 +1415,9 @@ class RaisesGroup(AbstractRaises[BaseExceptionGroup[BaseExcT_co]]):
|
||||
if exc_type is None:
|
||||
fail(f"DID NOT RAISE any exception, expected `{self.expected_type()}`")
|
||||
|
||||
assert self.excinfo is not None, (
|
||||
"Internal error - should have been constructed in __enter__"
|
||||
)
|
||||
assert (
|
||||
self.excinfo is not None
|
||||
), "Internal error - should have been constructed in __enter__"
|
||||
|
||||
# group_str is the only thing that differs between RaisesExc and RaisesGroup...
|
||||
# I might just scrap it? Or make it part of fail_reason
|
||||
|
||||
@ -15,7 +15,6 @@ from typing import overload
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
@ -26,7 +25,6 @@ from _pytest.fixtures import fixture
|
||||
from _pytest.outcomes import Exit
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
|
||||
@ -35,7 +35,6 @@ from _pytest.nodes import Item
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import skip
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
@ -274,9 +273,9 @@ def _format_exception_group_all_skipped_longrepr(
|
||||
excinfo: ExceptionInfo[BaseExceptionGroup[BaseException | BaseExceptionGroup]],
|
||||
) -> tuple[str, int, str]:
|
||||
r = excinfo._getreprcrash()
|
||||
assert r is not None, (
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
assert (
|
||||
r is not None
|
||||
), "There should always be a traceback entry for skipping a test."
|
||||
if all(
|
||||
getattr(skip, "_use_item_location", False) for skip in excinfo.value.exceptions
|
||||
):
|
||||
@ -321,11 +320,13 @@ class TestReport(BaseReport):
|
||||
location: tuple[str, int | None, str],
|
||||
keywords: Mapping[str, Any],
|
||||
outcome: Literal["passed", "failed", "skipped"],
|
||||
longrepr: None
|
||||
longrepr: (
|
||||
None
|
||||
| ExceptionInfo[BaseException]
|
||||
| tuple[str, int, str]
|
||||
| str
|
||||
| TerminalRepr,
|
||||
| TerminalRepr
|
||||
),
|
||||
when: Literal["setup", "call", "teardown"],
|
||||
sections: Iterable[tuple[str, str]] = (),
|
||||
duration: float = 0,
|
||||
@ -412,9 +413,9 @@ class TestReport(BaseReport):
|
||||
elif isinstance(excinfo.value, skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
assert r is not None, (
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
assert (
|
||||
r is not None
|
||||
), "There should always be a traceback entry for skipping a test."
|
||||
if excinfo.value._use_item_location:
|
||||
path, line = item.reportinfo()[:2]
|
||||
assert line is not None
|
||||
@ -466,11 +467,13 @@ class CollectReport(BaseReport):
|
||||
self,
|
||||
nodeid: str,
|
||||
outcome: Literal["passed", "failed", "skipped"],
|
||||
longrepr: None
|
||||
longrepr: (
|
||||
None
|
||||
| ExceptionInfo[BaseException]
|
||||
| tuple[str, int, str]
|
||||
| str
|
||||
| TerminalRepr,
|
||||
| TerminalRepr
|
||||
),
|
||||
result: list[Item | Collector] | None,
|
||||
sections: Iterable[tuple[str, str]] = (),
|
||||
**extra,
|
||||
@ -496,7 +499,7 @@ class CollectReport(BaseReport):
|
||||
self.__dict__.update(extra)
|
||||
|
||||
@property
|
||||
def location( # type:ignore[override]
|
||||
def location( # type: ignore[override]
|
||||
self,
|
||||
) -> tuple[str, int | None, str] | None:
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
@ -36,7 +36,6 @@ from _pytest.outcomes import OutcomeException
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
@ -172,7 +171,7 @@ def pytest_runtest_call(item: Item) -> None:
|
||||
del sys.last_value
|
||||
del sys.last_traceback
|
||||
if sys.version_info >= (3, 12, 0):
|
||||
del sys.last_exc # type:ignore[attr-defined]
|
||||
del sys.last_exc # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
@ -182,7 +181,7 @@ def pytest_runtest_call(item: Item) -> None:
|
||||
sys.last_type = type(e)
|
||||
sys.last_value = e
|
||||
if sys.version_info >= (3, 12, 0):
|
||||
sys.last_exc = e # type:ignore[attr-defined]
|
||||
sys.last_exc = e # type: ignore[attr-defined]
|
||||
assert e.__traceback__ is not None
|
||||
# Skip *this* frame
|
||||
sys.last_traceback = e.__traceback__.tb_next
|
||||
|
||||
@ -14,7 +14,6 @@ from enum import Enum
|
||||
from functools import total_ordering
|
||||
from typing import Literal
|
||||
|
||||
|
||||
_ScopeName = Literal["session", "package", "module", "class", "function"]
|
||||
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ from typing import cast
|
||||
from typing import Generic
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
__all__ = ["Stash", "StashKey"]
|
||||
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ from _pytest.config.argparsing import Parser
|
||||
from _pytest.main import Session
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
@ -38,7 +38,6 @@ from _pytest.runner import check_interactive_exception
|
||||
from _pytest.runner import get_reraise_exceptions
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
@ -53,7 +53,6 @@ from _pytest.reports import BaseReport
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.main import Session
|
||||
|
||||
@ -1231,7 +1230,7 @@ class TerminalReporter:
|
||||
return
|
||||
|
||||
session_duration = self._session_start.elapsed()
|
||||
(parts, main_color) = self.build_summary_stats_line()
|
||||
parts, main_color = self.build_summary_stats_line()
|
||||
line_parts = []
|
||||
|
||||
display_sep = self.verbosity >= 0
|
||||
@ -1460,7 +1459,9 @@ class TerminalReporter:
|
||||
|
||||
elif deselected == 0:
|
||||
main_color = "green"
|
||||
collected_output = "%d %s collected" % pluralize(self._numcollected, "test") # noqa: UP031
|
||||
collected_output = "%d %s collected" % pluralize(
|
||||
self._numcollected, "test"
|
||||
) # noqa: UP031
|
||||
parts = [(collected_output, {main_color: True})]
|
||||
else:
|
||||
all_tests_were_deselected = self._numcollected == deselected
|
||||
@ -1476,7 +1477,9 @@ class TerminalReporter:
|
||||
|
||||
if errors:
|
||||
main_color = _color_for_type["error"]
|
||||
parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})] # noqa: UP031
|
||||
parts += [
|
||||
("%d %s" % pluralize(errors, "error"), {main_color: True})
|
||||
] # noqa: UP031
|
||||
|
||||
return parts, main_color
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ from _pytest.stash import StashKey
|
||||
from _pytest.tracemalloc import tracemalloc_message
|
||||
import pytest
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ from time import sleep
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
@ -73,7 +72,8 @@ class MockTiming:
|
||||
uses `_pytest.timing` functions.
|
||||
|
||||
Time is static, and only advances through `sleep` calls, thus tests might sleep over large
|
||||
numbers and obtain accurate time() calls at the end, making tests reliable and instant."""
|
||||
numbers and obtain accurate time() calls at the end, making tests reliable and instant.
|
||||
"""
|
||||
|
||||
_current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp()
|
||||
|
||||
|
||||
@ -32,7 +32,6 @@ from _pytest.nodes import Item
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
tmppath_result_key = StashKey[dict[str, bool]]()
|
||||
RetentionType = Literal["all", "failed", "none"]
|
||||
|
||||
|
||||
@ -38,7 +38,6 @@ from _pytest.runner import check_interactive_exception
|
||||
from _pytest.subtests import SubtestContext
|
||||
from _pytest.subtests import SubtestReport
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
@ -405,9 +404,11 @@ class TestCaseFunction(Function):
|
||||
self,
|
||||
test_case: Any,
|
||||
test: TestCase,
|
||||
exc_info: ExceptionInfo[BaseException]
|
||||
exc_info: (
|
||||
ExceptionInfo[BaseException]
|
||||
| tuple[type[BaseException], BaseException, TracebackType]
|
||||
| None,
|
||||
| None
|
||||
),
|
||||
) -> None:
|
||||
exception_info: ExceptionInfo[BaseException] | None
|
||||
match exc_info:
|
||||
@ -459,9 +460,10 @@ class TestCaseFunction(Function):
|
||||
"""Compute or obtain the cached values for subtest errors and non-subtest skips."""
|
||||
from unittest.case import _SubTest # type: ignore[attr-defined]
|
||||
|
||||
assert sys.version_info < (3, 11), (
|
||||
"This workaround only should be used in Python 3.10"
|
||||
)
|
||||
assert sys.version_info < (
|
||||
3,
|
||||
11,
|
||||
), "This workaround only should be used in Python 3.10"
|
||||
if self._cached_errors_and_skips is not None:
|
||||
return self._cached_errors_and_skips
|
||||
|
||||
@ -600,7 +602,7 @@ def _handle_twisted_exc_info(
|
||||
# Unfortunately, because we cannot import `twisted.python.failure` at the top of the file
|
||||
# and use it in the signature, we need to use `type:ignore` here because we cannot narrow
|
||||
# the type properly in the `if` statement above.
|
||||
return rawexcinfo # type:ignore[return-value]
|
||||
return rawexcinfo # type: ignore[return-value]
|
||||
elif twisted_version is TwistedVersion.Version24:
|
||||
# Twisted calls addError() passing its own classes (like `twisted.python.Failure`), which violates
|
||||
# the `addError()` signature, so we extract the original `sys.exc_info()` tuple which is stored
|
||||
@ -609,8 +611,8 @@ def _handle_twisted_exc_info(
|
||||
saved_exc_info = getattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR)
|
||||
# Delete the attribute from the original object to avoid leaks.
|
||||
delattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR)
|
||||
return saved_exc_info # type:ignore[no-any-return]
|
||||
return rawexcinfo # type:ignore[return-value]
|
||||
return saved_exc_info # type: ignore[no-any-return]
|
||||
return rawexcinfo # type: ignore[return-value]
|
||||
elif twisted_version is TwistedVersion.Version25:
|
||||
if isinstance(rawexcinfo, BaseException):
|
||||
import twisted.python.failure
|
||||
@ -621,7 +623,7 @@ def _handle_twisted_exc_info(
|
||||
tb = sys.exc_info()[2]
|
||||
return type(rawexcinfo.value), rawexcinfo.value, tb
|
||||
|
||||
return rawexcinfo # type:ignore[return-value]
|
||||
return rawexcinfo # type: ignore[return-value]
|
||||
else:
|
||||
# Ideally we would use assert_never() here, but it is not available in all Python versions
|
||||
# we support, plus we do not require `type_extensions` currently.
|
||||
|
||||
@ -16,7 +16,6 @@ from _pytest.stash import StashKey
|
||||
from _pytest.tracemalloc import tracemalloc_message
|
||||
import pytest
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import yaml
|
||||
|
||||
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
||||
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
||||
if not getattr(yaml, '__with_libyaml__', False):
|
||||
if not getattr(yaml, "__with_libyaml__", False):
|
||||
from sys import version_info
|
||||
|
||||
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
||||
@ -15,19 +15,20 @@ if not getattr(yaml, '__with_libyaml__', False):
|
||||
else:
|
||||
from yaml._yaml import *
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
'The _yaml extension module is now located at yaml._yaml'
|
||||
' and its location is subject to change. To use the'
|
||||
' LibYAML-based parser and emitter, import from `yaml`:'
|
||||
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
||||
DeprecationWarning
|
||||
"The _yaml extension module is now located at yaml._yaml"
|
||||
" and its location is subject to change. To use the"
|
||||
" LibYAML-based parser and emitter, import from `yaml`:"
|
||||
" `from yaml import CLoader as Loader, CDumper as Dumper`.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
del warnings
|
||||
# Don't `del yaml` here because yaml is actually an existing
|
||||
# namespace member of _yaml.
|
||||
|
||||
__name__ = '_yaml'
|
||||
__name__ = "_yaml"
|
||||
# If the module is top-level (i.e. not a part of any specific package)
|
||||
# then the attribute should be set to ''.
|
||||
# https://docs.python.org/3.8/library/types.html
|
||||
__package__ = ''
|
||||
__package__ = ""
|
||||
|
||||
@ -3,4 +3,3 @@ Generator: hatchling 1.28.0
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-macosx_11_0_arm64
|
||||
Generator: delocate 0.13.0
|
||||
|
||||
|
||||
@ -13,13 +13,16 @@ from black.mode import Mode
|
||||
from black.output import out
|
||||
from black.report import NothingChanged
|
||||
|
||||
TRANSFORMED_MAGICS = frozenset((
|
||||
TRANSFORMED_MAGICS = frozenset(
|
||||
(
|
||||
"get_ipython().run_cell_magic",
|
||||
"get_ipython().system",
|
||||
"get_ipython().getoutput",
|
||||
"get_ipython().run_line_magic",
|
||||
))
|
||||
TOKENS_TO_IGNORE = frozenset((
|
||||
)
|
||||
)
|
||||
TOKENS_TO_IGNORE = frozenset(
|
||||
(
|
||||
"ENDMARKER",
|
||||
"NL",
|
||||
"NEWLINE",
|
||||
@ -27,8 +30,10 @@ TOKENS_TO_IGNORE = frozenset((
|
||||
"DEDENT",
|
||||
"UNIMPORTANT_WS",
|
||||
"ESCAPED_NL",
|
||||
))
|
||||
PYTHON_CELL_MAGICS = frozenset((
|
||||
)
|
||||
)
|
||||
PYTHON_CELL_MAGICS = frozenset(
|
||||
(
|
||||
"capture",
|
||||
"prun",
|
||||
"pypy",
|
||||
@ -36,7 +41,8 @@ PYTHON_CELL_MAGICS = frozenset((
|
||||
"python3",
|
||||
"time",
|
||||
"timeit",
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
|
||||
@ -309,7 +309,8 @@ class Mode:
|
||||
return ".".join(parts)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((
|
||||
return hash(
|
||||
(
|
||||
frozenset(self.target_versions),
|
||||
self.line_length,
|
||||
self.string_normalization,
|
||||
@ -321,4 +322,5 @@ class Mode:
|
||||
self.preview,
|
||||
self.unstable,
|
||||
frozenset(self.enabled_features),
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
@ -3,4 +3,3 @@ Generator: setuptools (75.5.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
|
||||
@ -14,17 +14,17 @@ class ValidationError(ValueError):
|
||||
self.ctx = ctx
|
||||
|
||||
def __str__(self):
|
||||
out = '\n'
|
||||
out = "\n"
|
||||
err = self
|
||||
while err.ctx is not None:
|
||||
out += f'==> {err.ctx}\n'
|
||||
out += f"==> {err.ctx}\n"
|
||||
err = err.error_msg
|
||||
out += f'=====> {err.error_msg}'
|
||||
out += f"=====> {err.error_msg}"
|
||||
return out
|
||||
|
||||
|
||||
MISSING = collections.namedtuple('Missing', ())()
|
||||
type(MISSING).__repr__ = lambda self: 'MISSING'
|
||||
MISSING = collections.namedtuple("Missing", ())()
|
||||
type(MISSING).__repr__ = lambda self: "MISSING"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@ -52,7 +52,7 @@ def _dct_noop(self, dct):
|
||||
def _check_optional(self, dct):
|
||||
if self.key not in dct:
|
||||
return
|
||||
with validate_context(f'At key: {self.key}'):
|
||||
with validate_context(f"At key: {self.key}"):
|
||||
self.check_fn(dct[self.key])
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ def _remove_default_optional(self, dct):
|
||||
|
||||
def _require_key(self, dct):
|
||||
if self.key not in dct:
|
||||
raise ValidationError(f'Missing required key: {self.key}')
|
||||
raise ValidationError(f"Missing required key: {self.key}")
|
||||
|
||||
|
||||
def _check_required(self, dct):
|
||||
@ -79,6 +79,7 @@ def _check_required(self, dct):
|
||||
def _check_fn_recurse(self):
|
||||
def check_fn(val):
|
||||
validate(val, self.schema)
|
||||
|
||||
return check_fn
|
||||
|
||||
|
||||
@ -106,18 +107,16 @@ def _get_check_conditional(inner):
|
||||
def _check_conditional(self, dct):
|
||||
if dct.get(self.condition_key, MISSING) == self.condition_value:
|
||||
inner(self, dct)
|
||||
elif (
|
||||
self.condition_key in dct and
|
||||
self.ensure_absent and self.key in dct
|
||||
):
|
||||
if hasattr(self.condition_value, 'describe_opposite'):
|
||||
elif self.condition_key in dct and self.ensure_absent and self.key in dct:
|
||||
if hasattr(self.condition_value, "describe_opposite"):
|
||||
explanation = self.condition_value.describe_opposite()
|
||||
else:
|
||||
explanation = f'is not {self.condition_value!r}'
|
||||
explanation = f"is not {self.condition_value!r}"
|
||||
raise ValidationError(
|
||||
f'Expected {self.key} to be absent when {self.condition_key} '
|
||||
f'{explanation}, found {self.key}: {dct[self.key]!r}',
|
||||
f"Expected {self.key} to be absent when {self.condition_key} "
|
||||
f"{explanation}, found {self.key}: {dct[self.key]!r}",
|
||||
)
|
||||
|
||||
return _check_conditional
|
||||
|
||||
|
||||
@ -144,11 +143,11 @@ def _remove_default_conditional_recurse(self, dct):
|
||||
def _no_additional_keys_check(self, dct):
|
||||
extra = sorted(set(dct) - set(self.keys))
|
||||
if extra:
|
||||
extra_s = ', '.join(str(x) for x in extra)
|
||||
keys_s = ', '.join(str(x) for x in self.keys)
|
||||
extra_s = ", ".join(str(x) for x in extra)
|
||||
keys_s = ", ".join(str(x) for x in self.keys)
|
||||
raise ValidationError(
|
||||
f'Additional keys found: {extra_s}. '
|
||||
f'Only these keys are allowed: {keys_s}',
|
||||
f"Additional keys found: {extra_s}. "
|
||||
f"Only these keys are allowed: {keys_s}",
|
||||
)
|
||||
|
||||
|
||||
@ -158,45 +157,51 @@ def _warn_additional_keys_check(self, dct):
|
||||
self.callback(extra, self.keys, dct)
|
||||
|
||||
|
||||
Required = collections.namedtuple('Required', ('key', 'check_fn'))
|
||||
Required = collections.namedtuple("Required", ("key", "check_fn"))
|
||||
Required.check = _check_required
|
||||
Required.apply_default = _dct_noop
|
||||
Required.remove_default = _dct_noop
|
||||
RequiredRecurse = collections.namedtuple('RequiredRecurse', ('key', 'schema'))
|
||||
RequiredRecurse = collections.namedtuple("RequiredRecurse", ("key", "schema"))
|
||||
RequiredRecurse.check = _check_required
|
||||
RequiredRecurse.check_fn = _check_fn_recurse
|
||||
RequiredRecurse.apply_default = _apply_default_required_recurse
|
||||
RequiredRecurse.remove_default = _remove_default_required_recurse
|
||||
Optional = collections.namedtuple('Optional', ('key', 'check_fn', 'default'))
|
||||
Optional = collections.namedtuple("Optional", ("key", "check_fn", "default"))
|
||||
Optional.check = _check_optional
|
||||
Optional.apply_default = _apply_default_optional
|
||||
Optional.remove_default = _remove_default_optional
|
||||
OptionalRecurse = collections.namedtuple(
|
||||
'OptionalRecurse', ('key', 'schema', 'default'),
|
||||
"OptionalRecurse",
|
||||
("key", "schema", "default"),
|
||||
)
|
||||
OptionalRecurse.check = _check_optional
|
||||
OptionalRecurse.check_fn = _check_fn_recurse
|
||||
OptionalRecurse.apply_default = _apply_default_optional_recurse
|
||||
OptionalRecurse.remove_default = _remove_default_optional_recurse
|
||||
OptionalNoDefault = collections.namedtuple(
|
||||
'OptionalNoDefault', ('key', 'check_fn'),
|
||||
"OptionalNoDefault",
|
||||
("key", "check_fn"),
|
||||
)
|
||||
OptionalNoDefault.check = _check_optional
|
||||
OptionalNoDefault.apply_default = _dct_noop
|
||||
OptionalNoDefault.remove_default = _dct_noop
|
||||
Conditional = collections.namedtuple(
|
||||
'Conditional',
|
||||
('key', 'check_fn', 'condition_key', 'condition_value', 'ensure_absent'),
|
||||
"Conditional",
|
||||
("key", "check_fn", "condition_key", "condition_value", "ensure_absent"),
|
||||
)
|
||||
Conditional.__new__.__defaults__ = (False,)
|
||||
Conditional.check = _get_check_conditional(_check_required)
|
||||
Conditional.apply_default = _dct_noop
|
||||
Conditional.remove_default = _dct_noop
|
||||
ConditionalOptional = collections.namedtuple(
|
||||
'ConditionalOptional',
|
||||
"ConditionalOptional",
|
||||
(
|
||||
'key', 'check_fn', 'default', 'condition_key', 'condition_value',
|
||||
'ensure_absent',
|
||||
"key",
|
||||
"check_fn",
|
||||
"default",
|
||||
"condition_key",
|
||||
"condition_value",
|
||||
"ensure_absent",
|
||||
),
|
||||
)
|
||||
ConditionalOptional.__new__.__defaults__ = (False,)
|
||||
@ -204,27 +209,28 @@ ConditionalOptional.check = _get_check_conditional(_check_optional)
|
||||
ConditionalOptional.apply_default = _apply_default_conditional_optional
|
||||
ConditionalOptional.remove_default = _remove_default_conditional_optional
|
||||
ConditionalRecurse = collections.namedtuple(
|
||||
'ConditionalRecurse',
|
||||
('key', 'schema', 'condition_key', 'condition_value', 'ensure_absent'),
|
||||
"ConditionalRecurse",
|
||||
("key", "schema", "condition_key", "condition_value", "ensure_absent"),
|
||||
)
|
||||
ConditionalRecurse.__new__.__defaults__ = (False,)
|
||||
ConditionalRecurse.check = _get_check_conditional(_check_required)
|
||||
ConditionalRecurse.check_fn = _check_fn_recurse
|
||||
ConditionalRecurse.apply_default = _apply_default_conditional_recurse
|
||||
ConditionalRecurse.remove_default = _remove_default_conditional_recurse
|
||||
NoAdditionalKeys = collections.namedtuple('NoAdditionalKeys', ('keys',))
|
||||
NoAdditionalKeys = collections.namedtuple("NoAdditionalKeys", ("keys",))
|
||||
NoAdditionalKeys.check = _no_additional_keys_check
|
||||
NoAdditionalKeys.apply_default = _dct_noop
|
||||
NoAdditionalKeys.remove_default = _dct_noop
|
||||
WarnAdditionalKeys = collections.namedtuple(
|
||||
'WarnAdditionalKeys', ('keys', 'callback'),
|
||||
"WarnAdditionalKeys",
|
||||
("keys", "callback"),
|
||||
)
|
||||
WarnAdditionalKeys.check = _warn_additional_keys_check
|
||||
WarnAdditionalKeys.apply_default = _dct_noop
|
||||
WarnAdditionalKeys.remove_default = _dct_noop
|
||||
|
||||
|
||||
class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))):
|
||||
class Map(collections.namedtuple("Map", ("object_name", "id_key", "items"))):
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, object_name, id_key, *items):
|
||||
@ -233,14 +239,13 @@ class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))):
|
||||
def check(self, v):
|
||||
if not isinstance(v, dict):
|
||||
raise ValidationError(
|
||||
f'Expected a {self.object_name} map but got a '
|
||||
f'{type(v).__name__}',
|
||||
f"Expected a {self.object_name} map but got a " f"{type(v).__name__}",
|
||||
)
|
||||
if self.id_key is None:
|
||||
context = f'At {self.object_name}()'
|
||||
context = f"At {self.object_name}()"
|
||||
else:
|
||||
key_v_s = v.get(self.id_key, MISSING)
|
||||
context = f'At {self.object_name}({self.id_key}={key_v_s!r})'
|
||||
context = f"At {self.object_name}({self.id_key}={key_v_s!r})"
|
||||
with validate_context(context):
|
||||
for item in self.items:
|
||||
item.check(v)
|
||||
@ -260,8 +265,8 @@ class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))):
|
||||
|
||||
class KeyValueMap(
|
||||
collections.namedtuple(
|
||||
'KeyValueMap',
|
||||
('object_name', 'check_key_fn', 'value_schema'),
|
||||
"KeyValueMap",
|
||||
("object_name", "check_key_fn", "value_schema"),
|
||||
),
|
||||
):
|
||||
__slots__ = ()
|
||||
@ -269,30 +274,23 @@ class KeyValueMap(
|
||||
def check(self, v):
|
||||
if not isinstance(v, dict):
|
||||
raise ValidationError(
|
||||
f'Expected a {self.object_name} map but got a '
|
||||
f'{type(v).__name__}',
|
||||
f"Expected a {self.object_name} map but got a " f"{type(v).__name__}",
|
||||
)
|
||||
with validate_context(f'At {self.object_name}()'):
|
||||
with validate_context(f"At {self.object_name}()"):
|
||||
for k, val in v.items():
|
||||
with validate_context(f'For key: {k}'):
|
||||
with validate_context(f"For key: {k}"):
|
||||
self.check_key_fn(k)
|
||||
with validate_context(f'At key: {k}'):
|
||||
with validate_context(f"At key: {k}"):
|
||||
validate(val, self.value_schema)
|
||||
|
||||
def apply_defaults(self, v):
|
||||
return {
|
||||
k: apply_defaults(val, self.value_schema)
|
||||
for k, val in v.items()
|
||||
}
|
||||
return {k: apply_defaults(val, self.value_schema) for k, val in v.items()}
|
||||
|
||||
def remove_defaults(self, v):
|
||||
return {
|
||||
k: remove_defaults(val, self.value_schema)
|
||||
for k, val in v.items()
|
||||
}
|
||||
return {k: remove_defaults(val, self.value_schema) for k, val in v.items()}
|
||||
|
||||
|
||||
class Array(collections.namedtuple('Array', ('of', 'allow_empty'))):
|
||||
class Array(collections.namedtuple("Array", ("of", "allow_empty"))):
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, of, allow_empty=True):
|
||||
@ -314,37 +312,37 @@ class Array(collections.namedtuple('Array', ('of', 'allow_empty'))):
|
||||
return [remove_defaults(val, self.of) for val in v]
|
||||
|
||||
|
||||
class Not(collections.namedtuple('Not', ('val',))):
|
||||
class Not(collections.namedtuple("Not", ("val",))):
|
||||
__slots__ = ()
|
||||
|
||||
def describe_opposite(self):
|
||||
return f'is {self.val!r}'
|
||||
return f"is {self.val!r}"
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not MISSING and other != self.val
|
||||
|
||||
|
||||
class NotIn(collections.namedtuple('NotIn', ('values',))):
|
||||
class NotIn(collections.namedtuple("NotIn", ("values",))):
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, *values):
|
||||
return super().__new__(cls, values=values)
|
||||
|
||||
def describe_opposite(self):
|
||||
return f'is any of {self.values!r}'
|
||||
return f"is any of {self.values!r}"
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not MISSING and other not in self.values
|
||||
|
||||
|
||||
class In(collections.namedtuple('In', ('values',))):
|
||||
class In(collections.namedtuple("In", ("values",))):
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, *values):
|
||||
return super().__new__(cls, values=values)
|
||||
|
||||
def describe_opposite(self):
|
||||
return f'is not any of {self.values!r}'
|
||||
return f"is not any of {self.values!r}"
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not MISSING and other in self.values
|
||||
@ -359,25 +357,27 @@ def check_type(tp, typename=None):
|
||||
if not isinstance(v, tp):
|
||||
typename_s = typename or tp.__name__
|
||||
raise ValidationError(
|
||||
f'Expected {typename_s} got {type(v).__name__}',
|
||||
f"Expected {typename_s} got {type(v).__name__}",
|
||||
)
|
||||
|
||||
return check_type_fn
|
||||
|
||||
|
||||
check_bool = check_type(bool)
|
||||
check_bytes = check_type(bytes)
|
||||
check_int = check_type(int)
|
||||
check_string = check_type(str, typename='string')
|
||||
check_text = check_type(str, typename='text')
|
||||
check_string = check_type(str, typename="string")
|
||||
check_text = check_type(str, typename="text")
|
||||
|
||||
|
||||
def check_one_of(possible):
|
||||
def check_one_of_fn(v):
|
||||
if v not in possible:
|
||||
possible_s = ', '.join(str(x) for x in sorted(possible))
|
||||
possible_s = ", ".join(str(x) for x in sorted(possible))
|
||||
raise ValidationError(
|
||||
f'Expected one of {possible_s} but got: {v!r}',
|
||||
f"Expected one of {possible_s} but got: {v!r}",
|
||||
)
|
||||
|
||||
return check_one_of_fn
|
||||
|
||||
|
||||
@ -385,19 +385,20 @@ def check_regex(v):
|
||||
try:
|
||||
re.compile(v)
|
||||
except re.error:
|
||||
raise ValidationError(f'{v!r} is not a valid python regex')
|
||||
raise ValidationError(f"{v!r} is not a valid python regex")
|
||||
|
||||
|
||||
def check_array(inner_check):
|
||||
def check_array_fn(v):
|
||||
if not isinstance(v, (list, tuple)):
|
||||
raise ValidationError(
|
||||
f'Expected array but got {type(v).__name__!r}',
|
||||
f"Expected array but got {type(v).__name__!r}",
|
||||
)
|
||||
|
||||
for i, val in enumerate(v):
|
||||
with validate_context(f'At index {i}'):
|
||||
with validate_context(f"At index {i}"):
|
||||
inner_check(val)
|
||||
|
||||
return check_array_fn
|
||||
|
||||
|
||||
@ -405,6 +406,7 @@ def check_and(*fns):
|
||||
def check(v):
|
||||
for fn in fns:
|
||||
fn(v)
|
||||
|
||||
return check
|
||||
|
||||
|
||||
@ -432,11 +434,11 @@ def load_from_filename(
|
||||
display_filename = display_filename or filename
|
||||
with reraise_as(exc_tp):
|
||||
if not os.path.isfile(filename):
|
||||
raise ValidationError(f'{display_filename} is not a file')
|
||||
raise ValidationError(f"{display_filename} is not a file")
|
||||
|
||||
with validate_context(f'File {display_filename}'):
|
||||
with validate_context(f"File {display_filename}"):
|
||||
try:
|
||||
with open(filename, encoding='utf-8') as f:
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
contents = f.read()
|
||||
except UnicodeDecodeError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
@ -81,4 +81,3 @@ contribute, including reporting issues, requesting features, asking or answering
|
||||
questions, and making PRs.
|
||||
|
||||
[contrib]: https://palletsprojects.com/contributing/
|
||||
|
||||
|
||||
@ -1556,9 +1556,9 @@ class Group(Command):
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = None,
|
||||
commands: cabc.MutableMapping[str, Command]
|
||||
| cabc.Sequence[Command]
|
||||
| None = None,
|
||||
commands: (
|
||||
cabc.MutableMapping[str, Command] | cabc.Sequence[Command] | None
|
||||
) = None,
|
||||
invoke_without_command: bool = False,
|
||||
no_args_is_help: bool | None = None,
|
||||
subcommand_metavar: str | None = None,
|
||||
@ -1659,9 +1659,9 @@ class Group(Command):
|
||||
func: t.Callable[..., t.Any] | None = None
|
||||
|
||||
if args and callable(args[0]):
|
||||
assert len(args) == 1 and not kwargs, (
|
||||
"Use 'command(**kwargs)(callable)' to provide arguments."
|
||||
)
|
||||
assert (
|
||||
len(args) == 1 and not kwargs
|
||||
), "Use 'command(**kwargs)(callable)' to provide arguments."
|
||||
(func,) = args
|
||||
args = ()
|
||||
|
||||
@ -1708,9 +1708,9 @@ class Group(Command):
|
||||
func: t.Callable[..., t.Any] | None = None
|
||||
|
||||
if args and callable(args[0]):
|
||||
assert len(args) == 1 and not kwargs, (
|
||||
"Use 'group(**kwargs)(callable)' to provide arguments."
|
||||
)
|
||||
assert (
|
||||
len(args) == 1 and not kwargs
|
||||
), "Use 'group(**kwargs)(callable)' to provide arguments."
|
||||
(func,) = args
|
||||
args = ()
|
||||
|
||||
@ -2140,10 +2140,10 @@ class Parameter:
|
||||
expose_value: bool = True,
|
||||
is_eager: bool = False,
|
||||
envvar: str | cabc.Sequence[str] | None = None,
|
||||
shell_complete: t.Callable[
|
||||
[Context, Parameter, str], list[CompletionItem] | list[str]
|
||||
]
|
||||
| None = None,
|
||||
shell_complete: (
|
||||
t.Callable[[Context, Parameter, str], list[CompletionItem] | list[str]]
|
||||
| None
|
||||
) = None,
|
||||
deprecated: bool | str = False,
|
||||
) -> None:
|
||||
self.name: str | None
|
||||
@ -2594,9 +2594,9 @@ class Parameter:
|
||||
):
|
||||
# Click is logically enforcing that the name is None if the parameter is
|
||||
# not to be exposed. We still assert it here to please the type checker.
|
||||
assert self.name is not None, (
|
||||
f"{self!r} parameter's name should not be None when exposing value."
|
||||
)
|
||||
assert (
|
||||
self.name is not None
|
||||
), f"{self!r} parameter's name should not be None when exposing value."
|
||||
ctx.params[self.name] = value
|
||||
|
||||
return value, args
|
||||
|
||||
@ -179,8 +179,9 @@ class Result:
|
||||
return_value: t.Any,
|
||||
exit_code: int,
|
||||
exception: BaseException | None,
|
||||
exc_info: tuple[type[BaseException], BaseException, TracebackType]
|
||||
| None = None,
|
||||
exc_info: (
|
||||
tuple[type[BaseException], BaseException, TracebackType] | None
|
||||
) = None,
|
||||
):
|
||||
self.runner = runner
|
||||
self.stdout_bytes = stdout_bytes
|
||||
|
||||
@ -114,5 +114,3 @@ Everyone interacting in the distlib project's codebases, issue trackers, chat
|
||||
rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_.
|
||||
|
||||
.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
|
||||
|
||||
|
||||
|
||||
@ -3,4 +3,3 @@ Generator: bdist_wheel (0.37.1)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#
|
||||
import logging
|
||||
|
||||
__version__ = '0.4.0'
|
||||
__version__ = "0.4.0"
|
||||
|
||||
|
||||
class DistlibException(Exception):
|
||||
|
||||
@ -18,24 +18,41 @@ except ImportError: # pragma: no cover
|
||||
|
||||
if sys.version_info[0] < 3: # pragma: no cover
|
||||
from StringIO import StringIO
|
||||
string_types = basestring,
|
||||
|
||||
string_types = (basestring,)
|
||||
text_type = unicode
|
||||
from types import FileType as file_type
|
||||
import __builtin__ as builtins
|
||||
import ConfigParser as configparser
|
||||
from urlparse import urlparse, urlunparse, urljoin, urlsplit, urlunsplit
|
||||
from urllib import (urlretrieve, quote as _quote, unquote, url2pathname,
|
||||
pathname2url, ContentTooShortError, splittype)
|
||||
from urllib import (
|
||||
urlretrieve,
|
||||
quote as _quote,
|
||||
unquote,
|
||||
url2pathname,
|
||||
pathname2url,
|
||||
ContentTooShortError,
|
||||
splittype,
|
||||
)
|
||||
|
||||
def quote(s):
|
||||
if isinstance(s, unicode):
|
||||
s = s.encode('utf-8')
|
||||
s = s.encode("utf-8")
|
||||
return _quote(s)
|
||||
|
||||
import urllib2
|
||||
from urllib2 import (Request, urlopen, URLError, HTTPError,
|
||||
HTTPBasicAuthHandler, HTTPPasswordMgr, HTTPHandler,
|
||||
HTTPRedirectHandler, build_opener)
|
||||
from urllib2 import (
|
||||
Request,
|
||||
urlopen,
|
||||
URLError,
|
||||
HTTPError,
|
||||
HTTPBasicAuthHandler,
|
||||
HTTPPasswordMgr,
|
||||
HTTPHandler,
|
||||
HTTPRedirectHandler,
|
||||
build_opener,
|
||||
)
|
||||
|
||||
if ssl:
|
||||
from urllib2 import HTTPSHandler
|
||||
import httplib
|
||||
@ -43,6 +60,7 @@ if sys.version_info[0] < 3: # pragma: no cover
|
||||
import Queue as queue
|
||||
from HTMLParser import HTMLParser
|
||||
import htmlentitydefs
|
||||
|
||||
raw_input = raw_input
|
||||
from itertools import ifilter as filter
|
||||
from itertools import ifilterfalse as filterfalse
|
||||
@ -62,17 +80,35 @@ if sys.version_info[0] < 3: # pragma: no cover
|
||||
|
||||
else: # pragma: no cover
|
||||
from io import StringIO
|
||||
string_types = str,
|
||||
|
||||
string_types = (str,)
|
||||
text_type = str
|
||||
from io import TextIOWrapper as file_type
|
||||
import builtins
|
||||
import configparser
|
||||
from urllib.parse import (urlparse, urlunparse, urljoin, quote, unquote,
|
||||
urlsplit, urlunsplit, splittype)
|
||||
from urllib.request import (urlopen, urlretrieve, Request, url2pathname,
|
||||
pathname2url, HTTPBasicAuthHandler,
|
||||
HTTPPasswordMgr, HTTPHandler,
|
||||
HTTPRedirectHandler, build_opener)
|
||||
from urllib.parse import (
|
||||
urlparse,
|
||||
urlunparse,
|
||||
urljoin,
|
||||
quote,
|
||||
unquote,
|
||||
urlsplit,
|
||||
urlunsplit,
|
||||
splittype,
|
||||
)
|
||||
from urllib.request import (
|
||||
urlopen,
|
||||
urlretrieve,
|
||||
Request,
|
||||
url2pathname,
|
||||
pathname2url,
|
||||
HTTPBasicAuthHandler,
|
||||
HTTPPasswordMgr,
|
||||
HTTPHandler,
|
||||
HTTPRedirectHandler,
|
||||
build_opener,
|
||||
)
|
||||
|
||||
if ssl:
|
||||
from urllib.request import HTTPSHandler
|
||||
from urllib.error import HTTPError, URLError, ContentTooShortError
|
||||
@ -82,8 +118,10 @@ else: # pragma: no cover
|
||||
import queue
|
||||
from html.parser import HTMLParser
|
||||
import html.entities as htmlentitydefs
|
||||
|
||||
raw_input = input
|
||||
from itertools import filterfalse
|
||||
|
||||
filter = filter
|
||||
|
||||
try:
|
||||
@ -102,17 +140,18 @@ except ImportError: # pragma: no cover
|
||||
if not dn:
|
||||
return False
|
||||
|
||||
parts = dn.split('.')
|
||||
parts = dn.split(".")
|
||||
leftmost, remainder = parts[0], parts[1:]
|
||||
|
||||
wildcards = leftmost.count('*')
|
||||
wildcards = leftmost.count("*")
|
||||
if wildcards > max_wildcards:
|
||||
# Issue #17980: avoid denials of service by refusing more
|
||||
# than one wildcard per fragment. A survey of established
|
||||
# policy among SSL implementations showed it to be a
|
||||
# reasonable choice.
|
||||
raise CertificateError(
|
||||
"too many wildcards in certificate DNS name: " + repr(dn))
|
||||
"too many wildcards in certificate DNS name: " + repr(dn)
|
||||
)
|
||||
|
||||
# speed up common case w/o wildcards
|
||||
if not wildcards:
|
||||
@ -121,11 +160,11 @@ except ImportError: # pragma: no cover
|
||||
# RFC 6125, section 6.4.3, subitem 1.
|
||||
# The client SHOULD NOT attempt to match a presented identifier in which
|
||||
# the wildcard character comprises a label other than the left-most label.
|
||||
if leftmost == '*':
|
||||
if leftmost == "*":
|
||||
# When '*' is a fragment by itself, it matches a non-empty dotless
|
||||
# fragment.
|
||||
pats.append('[^.]+')
|
||||
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
|
||||
pats.append("[^.]+")
|
||||
elif leftmost.startswith("xn--") or hostname.startswith("xn--"):
|
||||
# RFC 6125, section 6.4.3, subitem 3.
|
||||
# The client SHOULD NOT attempt to match a presented identifier
|
||||
# where the wildcard character is embedded within an A-label or
|
||||
@ -133,13 +172,13 @@ except ImportError: # pragma: no cover
|
||||
pats.append(re.escape(leftmost))
|
||||
else:
|
||||
# Otherwise, '*' matches any dotless string, e.g. www*
|
||||
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
|
||||
pats.append(re.escape(leftmost).replace(r"\*", "[^.]*"))
|
||||
|
||||
# add the remaining fragments, ignore any wildcards
|
||||
for frag in remainder:
|
||||
pats.append(re.escape(frag))
|
||||
|
||||
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
|
||||
pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE)
|
||||
return pat.match(hostname)
|
||||
|
||||
def match_hostname(cert, hostname):
|
||||
@ -151,38 +190,43 @@ except ImportError: # pragma: no cover
|
||||
returns nothing.
|
||||
"""
|
||||
if not cert:
|
||||
raise ValueError("empty or no certificate, match_hostname needs a "
|
||||
raise ValueError(
|
||||
"empty or no certificate, match_hostname needs a "
|
||||
"SSL socket or SSL context with either "
|
||||
"CERT_OPTIONAL or CERT_REQUIRED")
|
||||
"CERT_OPTIONAL or CERT_REQUIRED"
|
||||
)
|
||||
dnsnames = []
|
||||
san = cert.get('subjectAltName', ())
|
||||
san = cert.get("subjectAltName", ())
|
||||
for key, value in san:
|
||||
if key == 'DNS':
|
||||
if key == "DNS":
|
||||
if _dnsname_match(value, hostname):
|
||||
return
|
||||
dnsnames.append(value)
|
||||
if not dnsnames:
|
||||
# The subject is only checked when there is no dNSName entry
|
||||
# in subjectAltName
|
||||
for sub in cert.get('subject', ()):
|
||||
for sub in cert.get("subject", ()):
|
||||
for key, value in sub:
|
||||
# XXX according to RFC 2818, the most specific Common Name
|
||||
# must be used.
|
||||
if key == 'commonName':
|
||||
if key == "commonName":
|
||||
if _dnsname_match(value, hostname):
|
||||
return
|
||||
dnsnames.append(value)
|
||||
if len(dnsnames) > 1:
|
||||
raise CertificateError("hostname %r "
|
||||
"doesn't match either of %s" %
|
||||
(hostname, ', '.join(map(repr, dnsnames))))
|
||||
raise CertificateError(
|
||||
"hostname %r "
|
||||
"doesn't match either of %s"
|
||||
% (hostname, ", ".join(map(repr, dnsnames)))
|
||||
)
|
||||
elif len(dnsnames) == 1:
|
||||
raise CertificateError("hostname %r "
|
||||
"doesn't match %r" %
|
||||
(hostname, dnsnames[0]))
|
||||
raise CertificateError(
|
||||
"hostname %r " "doesn't match %r" % (hostname, dnsnames[0])
|
||||
)
|
||||
else:
|
||||
raise CertificateError("no appropriate commonName or "
|
||||
"subjectAltName fields were found")
|
||||
raise CertificateError(
|
||||
"no appropriate commonName or " "subjectAltName fields were found"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
@ -217,7 +261,7 @@ except ImportError: # pragma: no cover
|
||||
# Additionally check that `file` is not a directory, as on Windows
|
||||
# directories pass the os.access check.
|
||||
def _access_check(fn, mode):
|
||||
return (os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn))
|
||||
return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)
|
||||
|
||||
# If we're given a path with a directory part, look it up directly rather
|
||||
# than referring to PATH directories. This includes checking relative to the
|
||||
@ -269,7 +313,7 @@ except ImportError: # pragma: no cover
|
||||
|
||||
from zipfile import ZipFile as BaseZipFile
|
||||
|
||||
if hasattr(BaseZipFile, '__enter__'): # pragma: no cover
|
||||
if hasattr(BaseZipFile, "__enter__"): # pragma: no cover
|
||||
ZipFile = BaseZipFile
|
||||
else: # pragma: no cover
|
||||
from zipfile import ZipExtFile as BaseZipExtFile
|
||||
@ -306,13 +350,13 @@ except ImportError: # pragma: no cover
|
||||
|
||||
def python_implementation():
|
||||
"""Return a string identifying the Python implementation."""
|
||||
if 'PyPy' in sys.version:
|
||||
return 'PyPy'
|
||||
if os.name == 'java':
|
||||
return 'Jython'
|
||||
if sys.version.startswith('IronPython'):
|
||||
return 'IronPython'
|
||||
return 'CPython'
|
||||
if "PyPy" in sys.version:
|
||||
return "PyPy"
|
||||
if os.name == "java":
|
||||
return "Jython"
|
||||
if sys.version.startswith("IronPython"):
|
||||
return "IronPython"
|
||||
return "CPython"
|
||||
|
||||
|
||||
import sysconfig
|
||||
@ -336,11 +380,11 @@ except AttributeError: # pragma: no cover
|
||||
# sys.getfilesystemencoding(): the return value is "the user’s preference
|
||||
# according to the result of nl_langinfo(CODESET), or None if the
|
||||
# nl_langinfo(CODESET) failed."
|
||||
_fsencoding = sys.getfilesystemencoding() or 'utf-8'
|
||||
if _fsencoding == 'mbcs':
|
||||
_fserrors = 'strict'
|
||||
_fsencoding = sys.getfilesystemencoding() or "utf-8"
|
||||
if _fsencoding == "mbcs":
|
||||
_fserrors = "strict"
|
||||
else:
|
||||
_fserrors = 'surrogateescape'
|
||||
_fserrors = "surrogateescape"
|
||||
|
||||
def fsencode(filename):
|
||||
if isinstance(filename, bytes):
|
||||
@ -348,8 +392,7 @@ except AttributeError: # pragma: no cover
|
||||
elif isinstance(filename, text_type):
|
||||
return filename.encode(_fsencoding, _fserrors)
|
||||
else:
|
||||
raise TypeError("expect bytes or str, not %s" %
|
||||
type(filename).__name__)
|
||||
raise TypeError("expect bytes or str, not %s" % type(filename).__name__)
|
||||
|
||||
def fsdecode(filename):
|
||||
if isinstance(filename, text_type):
|
||||
@ -357,8 +400,7 @@ except AttributeError: # pragma: no cover
|
||||
elif isinstance(filename, bytes):
|
||||
return filename.decode(_fsencoding, _fserrors)
|
||||
else:
|
||||
raise TypeError("expect bytes or str, not %s" %
|
||||
type(filename).__name__)
|
||||
raise TypeError("expect bytes or str, not %s" % type(filename).__name__)
|
||||
|
||||
|
||||
try:
|
||||
@ -374,8 +416,9 @@ except ImportError: # pragma: no cover
|
||||
enc = orig_enc[:12].lower().replace("_", "-")
|
||||
if enc == "utf-8" or enc.startswith("utf-8-"):
|
||||
return "utf-8"
|
||||
if enc in ("latin-1", "iso-8859-1", "iso-latin-1") or \
|
||||
enc.startswith(("latin-1-", "iso-8859-1-", "iso-latin-1-")):
|
||||
if enc in ("latin-1", "iso-8859-1", "iso-latin-1") or enc.startswith(
|
||||
("latin-1-", "iso-8859-1-", "iso-latin-1-")
|
||||
):
|
||||
return "iso-8859-1"
|
||||
return orig_enc
|
||||
|
||||
@ -402,24 +445,24 @@ except ImportError: # pragma: no cover
|
||||
filename = None
|
||||
bom_found = False
|
||||
encoding = None
|
||||
default = 'utf-8'
|
||||
default = "utf-8"
|
||||
|
||||
def read_or_stop():
|
||||
try:
|
||||
return readline()
|
||||
except StopIteration:
|
||||
return b''
|
||||
return b""
|
||||
|
||||
def find_cookie(line):
|
||||
try:
|
||||
# Decode as UTF-8. Either the line is an encoding declaration,
|
||||
# in which case it should be pure ASCII, or it must be UTF-8
|
||||
# per default encoding.
|
||||
line_string = line.decode('utf-8')
|
||||
line_string = line.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
msg = "invalid or missing encoding declaration"
|
||||
if filename is not None:
|
||||
msg = '{} for {!r}'.format(msg, filename)
|
||||
msg = "{} for {!r}".format(msg, filename)
|
||||
raise SyntaxError(msg)
|
||||
|
||||
matches = cookie_re.findall(line_string)
|
||||
@ -433,27 +476,25 @@ except ImportError: # pragma: no cover
|
||||
if filename is None:
|
||||
msg = "unknown encoding: " + encoding
|
||||
else:
|
||||
msg = "unknown encoding for {!r}: {}".format(
|
||||
filename, encoding)
|
||||
msg = "unknown encoding for {!r}: {}".format(filename, encoding)
|
||||
raise SyntaxError(msg)
|
||||
|
||||
if bom_found:
|
||||
if codec.name != 'utf-8':
|
||||
if codec.name != "utf-8":
|
||||
# This behaviour mimics the Python interpreter
|
||||
if filename is None:
|
||||
msg = 'encoding problem: utf-8'
|
||||
msg = "encoding problem: utf-8"
|
||||
else:
|
||||
msg = 'encoding problem for {!r}: utf-8'.format(
|
||||
filename)
|
||||
msg = "encoding problem for {!r}: utf-8".format(filename)
|
||||
raise SyntaxError(msg)
|
||||
encoding += '-sig'
|
||||
encoding += "-sig"
|
||||
return encoding
|
||||
|
||||
first = read_or_stop()
|
||||
if first.startswith(BOM_UTF8):
|
||||
bom_found = True
|
||||
first = first[3:]
|
||||
default = 'utf-8-sig'
|
||||
default = "utf-8-sig"
|
||||
if not first:
|
||||
return default, []
|
||||
|
||||
@ -491,11 +532,11 @@ except ImportError: # pragma: no cover
|
||||
from reprlib import recursive_repr as _recursive_repr
|
||||
except ImportError:
|
||||
|
||||
def _recursive_repr(fillvalue='...'):
|
||||
'''
|
||||
def _recursive_repr(fillvalue="..."):
|
||||
"""
|
||||
Decorator to make a repr function return fillvalue for a recursive
|
||||
call
|
||||
'''
|
||||
"""
|
||||
|
||||
def decorating_function(user_function):
|
||||
repr_running = set()
|
||||
@ -512,17 +553,16 @@ except ImportError: # pragma: no cover
|
||||
return result
|
||||
|
||||
# Can't use functools.wraps() here because of bootstrap issues
|
||||
wrapper.__module__ = getattr(user_function, '__module__')
|
||||
wrapper.__doc__ = getattr(user_function, '__doc__')
|
||||
wrapper.__name__ = getattr(user_function, '__name__')
|
||||
wrapper.__annotations__ = getattr(user_function,
|
||||
'__annotations__', {})
|
||||
wrapper.__module__ = getattr(user_function, "__module__")
|
||||
wrapper.__doc__ = getattr(user_function, "__doc__")
|
||||
wrapper.__name__ = getattr(user_function, "__name__")
|
||||
wrapper.__annotations__ = getattr(user_function, "__annotations__", {})
|
||||
return wrapper
|
||||
|
||||
return decorating_function
|
||||
|
||||
class ChainMap(MutableMapping):
|
||||
'''
|
||||
"""
|
||||
A ChainMap groups multiple dicts (or other mappings) together
|
||||
to create a single, updateable view.
|
||||
|
||||
@ -532,13 +572,13 @@ except ImportError: # pragma: no cover
|
||||
Lookups search the underlying mappings successively until a key is found.
|
||||
In contrast, writes, updates, and deletions only operate on the first
|
||||
mapping.
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self, *maps):
|
||||
'''Initialize a ChainMap by setting *maps* to the given mappings.
|
||||
"""Initialize a ChainMap by setting *maps* to the given mappings.
|
||||
If no mappings are provided, a single empty dictionary is used.
|
||||
|
||||
'''
|
||||
"""
|
||||
self.maps = list(maps) or [{}] # always at least one map
|
||||
|
||||
def __missing__(self, key):
|
||||
@ -547,19 +587,16 @@ except ImportError: # pragma: no cover
|
||||
def __getitem__(self, key):
|
||||
for mapping in self.maps:
|
||||
try:
|
||||
return mapping[
|
||||
key] # can't use 'key in mapping' with defaultdict
|
||||
return mapping[key] # can't use 'key in mapping' with defaultdict
|
||||
except KeyError:
|
||||
pass
|
||||
return self.__missing__(
|
||||
key) # support subclasses that define __missing__
|
||||
return self.__missing__(key) # support subclasses that define __missing__
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key] if key in self else default
|
||||
|
||||
def __len__(self):
|
||||
return len(set().union(
|
||||
*self.maps)) # reuses stored hash values if possible
|
||||
return len(set().union(*self.maps)) # reuses stored hash values if possible
|
||||
|
||||
def __iter__(self):
|
||||
return iter(set().union(*self.maps))
|
||||
@ -572,27 +609,28 @@ except ImportError: # pragma: no cover
|
||||
|
||||
@_recursive_repr()
|
||||
def __repr__(self):
|
||||
return '{0.__class__.__name__}({1})'.format(
|
||||
self, ', '.join(map(repr, self.maps)))
|
||||
return "{0.__class__.__name__}({1})".format(
|
||||
self, ", ".join(map(repr, self.maps))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, *args):
|
||||
'Create a ChainMap with a single dict created from the iterable.'
|
||||
"Create a ChainMap with a single dict created from the iterable."
|
||||
return cls(dict.fromkeys(iterable, *args))
|
||||
|
||||
def copy(self):
|
||||
'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]'
|
||||
"New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]"
|
||||
return self.__class__(self.maps[0].copy(), *self.maps[1:])
|
||||
|
||||
__copy__ = copy
|
||||
|
||||
def new_child(self): # like Django's Context.push()
|
||||
'New ChainMap with a new dict followed by all previous maps.'
|
||||
"New ChainMap with a new dict followed by all previous maps."
|
||||
return self.__class__({}, *self.maps)
|
||||
|
||||
@property
|
||||
def parents(self): # like Django's Context.pop()
|
||||
'New ChainMap from maps[1:].'
|
||||
"New ChainMap from maps[1:]."
|
||||
return self.__class__(*self.maps[1:])
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
@ -602,26 +640,24 @@ except ImportError: # pragma: no cover
|
||||
try:
|
||||
del self.maps[0][key]
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
'Key not found in the first mapping: {!r}'.format(key))
|
||||
raise KeyError("Key not found in the first mapping: {!r}".format(key))
|
||||
|
||||
def popitem(self):
|
||||
'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
|
||||
"Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty."
|
||||
try:
|
||||
return self.maps[0].popitem()
|
||||
except KeyError:
|
||||
raise KeyError('No keys found in the first mapping.')
|
||||
raise KeyError("No keys found in the first mapping.")
|
||||
|
||||
def pop(self, key, *args):
|
||||
'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'
|
||||
"Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0]."
|
||||
try:
|
||||
return self.maps[0].pop(key, *args)
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
'Key not found in the first mapping: {!r}'.format(key))
|
||||
raise KeyError("Key not found in the first mapping: {!r}".format(key))
|
||||
|
||||
def clear(self):
|
||||
'Clear maps[0], leaving maps[1:] intact.'
|
||||
"Clear maps[0], leaving maps[1:] intact."
|
||||
self.maps[0].clear()
|
||||
|
||||
|
||||
@ -630,13 +666,13 @@ try:
|
||||
except ImportError: # pragma: no cover
|
||||
|
||||
def cache_from_source(path, debug_override=None):
|
||||
assert path.endswith('.py')
|
||||
assert path.endswith(".py")
|
||||
if debug_override is None:
|
||||
debug_override = __debug__
|
||||
if debug_override:
|
||||
suffix = 'c'
|
||||
suffix = "c"
|
||||
else:
|
||||
suffix = 'o'
|
||||
suffix = "o"
|
||||
return path + suffix
|
||||
|
||||
|
||||
@ -657,7 +693,7 @@ except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
class OrderedDict(dict):
|
||||
'Dictionary that remembers insertion order'
|
||||
"Dictionary that remembers insertion order"
|
||||
|
||||
# An inherited dict maps keys to values.
|
||||
# The inherited dict provides __getitem__, __len__, __contains__, and get.
|
||||
@ -670,14 +706,13 @@ except ImportError: # pragma: no cover
|
||||
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
'''Initialize an ordered dictionary. Signature is the same as for
|
||||
"""Initialize an ordered dictionary. Signature is the same as for
|
||||
regular dictionaries, but keyword arguments are not recommended
|
||||
because their insertion order is arbitrary.
|
||||
|
||||
'''
|
||||
"""
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' %
|
||||
len(args))
|
||||
raise TypeError("expected at most 1 arguments, got %d" % len(args))
|
||||
try:
|
||||
self.__root
|
||||
except AttributeError:
|
||||
@ -687,7 +722,7 @@ except ImportError: # pragma: no cover
|
||||
self.__update(*args, **kwds)
|
||||
|
||||
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
|
||||
'od.__setitem__(i, y) <==> od[i]=y'
|
||||
"od.__setitem__(i, y) <==> od[i]=y"
|
||||
# Setting a new item creates a new link which goes at the end of the linked
|
||||
# list, and the inherited dictionary is updated with the new key/value pair.
|
||||
if key not in self:
|
||||
@ -697,7 +732,7 @@ except ImportError: # pragma: no cover
|
||||
dict_setitem(self, key, value)
|
||||
|
||||
def __delitem__(self, key, dict_delitem=dict.__delitem__):
|
||||
'od.__delitem__(y) <==> del od[y]'
|
||||
"od.__delitem__(y) <==> del od[y]"
|
||||
# Deleting an existing item uses self.__map to find the link which is
|
||||
# then removed by updating the links in the predecessor and successor nodes.
|
||||
dict_delitem(self, key)
|
||||
@ -706,7 +741,7 @@ except ImportError: # pragma: no cover
|
||||
link_next[0] = link_prev
|
||||
|
||||
def __iter__(self):
|
||||
'od.__iter__() <==> iter(od)'
|
||||
"od.__iter__() <==> iter(od)"
|
||||
root = self.__root
|
||||
curr = root[1]
|
||||
while curr is not root:
|
||||
@ -714,7 +749,7 @@ except ImportError: # pragma: no cover
|
||||
curr = curr[1]
|
||||
|
||||
def __reversed__(self):
|
||||
'od.__reversed__() <==> reversed(od)'
|
||||
"od.__reversed__() <==> reversed(od)"
|
||||
root = self.__root
|
||||
curr = root[0]
|
||||
while curr is not root:
|
||||
@ -722,7 +757,7 @@ except ImportError: # pragma: no cover
|
||||
curr = curr[0]
|
||||
|
||||
def clear(self):
|
||||
'od.clear() -> None. Remove all items from od.'
|
||||
"od.clear() -> None. Remove all items from od."
|
||||
try:
|
||||
for node in self.__map.itervalues():
|
||||
del node[:]
|
||||
@ -734,12 +769,12 @@ except ImportError: # pragma: no cover
|
||||
dict.clear(self)
|
||||
|
||||
def popitem(self, last=True):
|
||||
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
|
||||
"""od.popitem() -> (k, v), return and remove a (key, value) pair.
|
||||
Pairs are returned in LIFO order if last is true or FIFO order if false.
|
||||
|
||||
'''
|
||||
"""
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
raise KeyError("dictionary is empty")
|
||||
root = self.__root
|
||||
if last:
|
||||
link = root[0]
|
||||
@ -759,45 +794,47 @@ except ImportError: # pragma: no cover
|
||||
# -- the following methods do not depend on the internal structure --
|
||||
|
||||
def keys(self):
|
||||
'od.keys() -> list of keys in od'
|
||||
"od.keys() -> list of keys in od"
|
||||
return list(self)
|
||||
|
||||
def values(self):
|
||||
'od.values() -> list of values in od'
|
||||
"od.values() -> list of values in od"
|
||||
return [self[key] for key in self]
|
||||
|
||||
def items(self):
|
||||
'od.items() -> list of (key, value) pairs in od'
|
||||
"od.items() -> list of (key, value) pairs in od"
|
||||
return [(key, self[key]) for key in self]
|
||||
|
||||
def iterkeys(self):
|
||||
'od.iterkeys() -> an iterator over the keys in od'
|
||||
"od.iterkeys() -> an iterator over the keys in od"
|
||||
return iter(self)
|
||||
|
||||
def itervalues(self):
|
||||
'od.itervalues -> an iterator over the values in od'
|
||||
"od.itervalues -> an iterator over the values in od"
|
||||
for k in self:
|
||||
yield self[k]
|
||||
|
||||
def iteritems(self):
|
||||
'od.iteritems -> an iterator over the (key, value) items in od'
|
||||
"od.iteritems -> an iterator over the (key, value) items in od"
|
||||
for k in self:
|
||||
yield (k, self[k])
|
||||
|
||||
def update(*args, **kwds):
|
||||
'''od.update(E, **F) -> None. Update od from dict/iterable E and F.
|
||||
"""od.update(E, **F) -> None. Update od from dict/iterable E and F.
|
||||
|
||||
If E is a dict instance, does: for k in E: od[k] = E[k]
|
||||
If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
|
||||
Or if E is an iterable of items, does: for k, v in E: od[k] = v
|
||||
In either case, this is followed by: for k, v in F.items(): od[k] = v
|
||||
|
||||
'''
|
||||
"""
|
||||
if len(args) > 2:
|
||||
raise TypeError('update() takes at most 2 positional '
|
||||
'arguments (%d given)' % (len(args), ))
|
||||
raise TypeError(
|
||||
"update() takes at most 2 positional "
|
||||
"arguments (%d given)" % (len(args),)
|
||||
)
|
||||
elif not args:
|
||||
raise TypeError('update() takes at least 1 argument (0 given)')
|
||||
raise TypeError("update() takes at least 1 argument (0 given)")
|
||||
self = args[0]
|
||||
# Make progressively weaker assumptions about "other"
|
||||
other = ()
|
||||
@ -806,7 +843,7 @@ except ImportError: # pragma: no cover
|
||||
if isinstance(other, dict):
|
||||
for key in other:
|
||||
self[key] = other[key]
|
||||
elif hasattr(other, 'keys'):
|
||||
elif hasattr(other, "keys"):
|
||||
for key in other.keys():
|
||||
self[key] = other[key]
|
||||
else:
|
||||
@ -820,10 +857,10 @@ except ImportError: # pragma: no cover
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
||||
"""od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
||||
If key is not found, d is returned if given, otherwise KeyError is raised.
|
||||
|
||||
'''
|
||||
"""
|
||||
if key in self:
|
||||
result = self[key]
|
||||
del self[key]
|
||||
@ -833,60 +870,59 @@ except ImportError: # pragma: no cover
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
|
||||
"od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od"
|
||||
if key in self:
|
||||
return self[key]
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def __repr__(self, _repr_running=None):
|
||||
'od.__repr__() <==> repr(od)'
|
||||
"od.__repr__() <==> repr(od)"
|
||||
if not _repr_running:
|
||||
_repr_running = {}
|
||||
call_key = id(self), _get_ident()
|
||||
if call_key in _repr_running:
|
||||
return '...'
|
||||
return "..."
|
||||
_repr_running[call_key] = 1
|
||||
try:
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__, )
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
return "%s()" % (self.__class__.__name__,)
|
||||
return "%s(%r)" % (self.__class__.__name__, self.items())
|
||||
finally:
|
||||
del _repr_running[call_key]
|
||||
|
||||
def __reduce__(self):
|
||||
'Return state information for pickling'
|
||||
"Return state information for pickling"
|
||||
items = [[k, self[k]] for k in self]
|
||||
inst_dict = vars(self).copy()
|
||||
for k in vars(OrderedDict()):
|
||||
inst_dict.pop(k, None)
|
||||
if inst_dict:
|
||||
return (self.__class__, (items, ), inst_dict)
|
||||
return self.__class__, (items, )
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def copy(self):
|
||||
'od.copy() -> a shallow copy of od'
|
||||
"od.copy() -> a shallow copy of od"
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
|
||||
"""OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
|
||||
and values equal to v (which defaults to None).
|
||||
|
||||
'''
|
||||
"""
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
|
||||
"""od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
|
||||
while comparison to a regular mapping is order-insensitive.
|
||||
|
||||
'''
|
||||
"""
|
||||
if isinstance(other, OrderedDict):
|
||||
return len(self) == len(
|
||||
other) and self.items() == other.items()
|
||||
return len(self) == len(other) and self.items() == other.items()
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
@ -910,12 +946,12 @@ except ImportError: # pragma: no cover
|
||||
try:
|
||||
from logging.config import BaseConfigurator, valid_ident
|
||||
except ImportError: # pragma: no cover
|
||||
IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
|
||||
IDENTIFIER = re.compile("^[a-z_][a-z0-9_]*$", re.I)
|
||||
|
||||
def valid_ident(s):
|
||||
m = IDENTIFIER.match(s)
|
||||
if not m:
|
||||
raise ValueError('Not a valid Python identifier: %r' % s)
|
||||
raise ValueError("Not a valid Python identifier: %r" % s)
|
||||
return True
|
||||
|
||||
# The ConvertingXXX classes are wrappers around standard Python containers,
|
||||
@ -936,8 +972,7 @@ except ImportError: # pragma: no cover
|
||||
# If the converted value is different, save for next time
|
||||
if value is not result:
|
||||
self[key] = result
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
@ -948,8 +983,7 @@ except ImportError: # pragma: no cover
|
||||
# If the converted value is different, save for next time
|
||||
if value is not result:
|
||||
self[key] = result
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
@ -958,8 +992,7 @@ except ImportError: # pragma: no cover
|
||||
value = dict.pop(self, key, default)
|
||||
result = self.configurator.convert(value)
|
||||
if value is not result:
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
@ -973,8 +1006,7 @@ except ImportError: # pragma: no cover
|
||||
# If the converted value is different, save for next time
|
||||
if value is not result:
|
||||
self[key] = result
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
@ -983,8 +1015,7 @@ except ImportError: # pragma: no cover
|
||||
value = list.pop(self, idx)
|
||||
result = self.configurator.convert(value)
|
||||
if value is not result:
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple):
|
||||
result.parent = self
|
||||
return result
|
||||
|
||||
@ -995,8 +1026,7 @@ except ImportError: # pragma: no cover
|
||||
value = tuple.__getitem__(self, key)
|
||||
result = self.configurator.convert(value)
|
||||
if value is not result:
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
@ -1006,16 +1036,16 @@ except ImportError: # pragma: no cover
|
||||
The configurator base class which defines some useful defaults.
|
||||
"""
|
||||
|
||||
CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
|
||||
CONVERT_PATTERN = re.compile(r"^(?P<prefix>[a-z]+)://(?P<suffix>.*)$")
|
||||
|
||||
WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
|
||||
DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
|
||||
INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
|
||||
DIGIT_PATTERN = re.compile(r'^\d+$')
|
||||
WORD_PATTERN = re.compile(r"^\s*(\w+)\s*")
|
||||
DOT_PATTERN = re.compile(r"^\.\s*(\w+)\s*")
|
||||
INDEX_PATTERN = re.compile(r"^\[\s*(\w+)\s*\]\s*")
|
||||
DIGIT_PATTERN = re.compile(r"^\d+$")
|
||||
|
||||
value_converters = {
|
||||
'ext': 'ext_convert',
|
||||
'cfg': 'cfg_convert',
|
||||
"ext": "ext_convert",
|
||||
"cfg": "cfg_convert",
|
||||
}
|
||||
|
||||
# We might want to use a different one, e.g. importlib
|
||||
@ -1030,12 +1060,12 @@ except ImportError: # pragma: no cover
|
||||
Resolve strings to objects using standard import and attribute
|
||||
syntax.
|
||||
"""
|
||||
name = s.split('.')
|
||||
name = s.split(".")
|
||||
used = name.pop(0)
|
||||
try:
|
||||
found = self.importer(used)
|
||||
for frag in name:
|
||||
used += '.' + frag
|
||||
used += "." + frag
|
||||
try:
|
||||
found = getattr(found, frag)
|
||||
except AttributeError:
|
||||
@ -1044,7 +1074,7 @@ except ImportError: # pragma: no cover
|
||||
return found
|
||||
except ImportError:
|
||||
e, tb = sys.exc_info()[1:]
|
||||
v = ValueError('Cannot resolve %r: %s' % (s, e))
|
||||
v = ValueError("Cannot resolve %r: %s" % (s, e))
|
||||
v.__cause__, v.__traceback__ = e, tb
|
||||
raise v
|
||||
|
||||
@ -1059,7 +1089,7 @@ except ImportError: # pragma: no cover
|
||||
if m is None:
|
||||
raise ValueError("Unable to convert %r" % value)
|
||||
else:
|
||||
rest = rest[m.end():]
|
||||
rest = rest[m.end() :]
|
||||
d = self.config[m.groups()[0]]
|
||||
while rest:
|
||||
m = self.DOT_PATTERN.match(rest)
|
||||
@ -1073,17 +1103,16 @@ except ImportError: # pragma: no cover
|
||||
d = d[idx]
|
||||
else:
|
||||
try:
|
||||
n = int(
|
||||
idx
|
||||
) # try as number first (most likely)
|
||||
n = int(idx) # try as number first (most likely)
|
||||
d = d[n]
|
||||
except TypeError:
|
||||
d = d[idx]
|
||||
if m:
|
||||
rest = rest[m.end():]
|
||||
rest = rest[m.end() :]
|
||||
else:
|
||||
raise ValueError('Unable to convert '
|
||||
'%r at %r' % (value, rest))
|
||||
raise ValueError(
|
||||
"Unable to convert " "%r at %r" % (value, rest)
|
||||
)
|
||||
# rest should be empty
|
||||
return d
|
||||
|
||||
@ -1093,12 +1122,10 @@ except ImportError: # pragma: no cover
|
||||
replaced by their converting alternatives. Strings are checked to
|
||||
see if they have a conversion format and are converted if they do.
|
||||
"""
|
||||
if not isinstance(value, ConvertingDict) and isinstance(
|
||||
value, dict):
|
||||
if not isinstance(value, ConvertingDict) and isinstance(value, dict):
|
||||
value = ConvertingDict(value)
|
||||
value.configurator = self
|
||||
elif not isinstance(value, ConvertingList) and isinstance(
|
||||
value, list):
|
||||
elif not isinstance(value, ConvertingList) and isinstance(value, list):
|
||||
value = ConvertingList(value)
|
||||
value.configurator = self
|
||||
elif not isinstance(value, ConvertingTuple) and isinstance(value, tuple):
|
||||
@ -1108,20 +1135,20 @@ except ImportError: # pragma: no cover
|
||||
m = self.CONVERT_PATTERN.match(value)
|
||||
if m:
|
||||
d = m.groupdict()
|
||||
prefix = d['prefix']
|
||||
prefix = d["prefix"]
|
||||
converter = self.value_converters.get(prefix, None)
|
||||
if converter:
|
||||
suffix = d['suffix']
|
||||
suffix = d["suffix"]
|
||||
converter = getattr(self, converter)
|
||||
value = converter(suffix)
|
||||
return value
|
||||
|
||||
def configure_custom(self, config):
|
||||
"""Configure an object with a user-supplied factory."""
|
||||
c = config.pop('()')
|
||||
c = config.pop("()")
|
||||
if not callable(c):
|
||||
c = self.resolve(c)
|
||||
props = config.pop('.', None)
|
||||
props = config.pop(".", None)
|
||||
# Check for valid identifiers
|
||||
kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
|
||||
result = c(**kwargs)
|
||||
|
||||
@ -20,22 +20,46 @@ import zipimport
|
||||
from . import DistlibException, resources
|
||||
from .compat import StringIO
|
||||
from .version import get_scheme, UnsupportedVersionError
|
||||
from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME)
|
||||
from .util import (parse_requirement, cached_property, parse_name_and_version, read_exports, write_exports, CSVReader,
|
||||
CSVWriter)
|
||||
from .metadata import (
|
||||
Metadata,
|
||||
METADATA_FILENAME,
|
||||
WHEEL_METADATA_FILENAME,
|
||||
LEGACY_METADATA_FILENAME,
|
||||
)
|
||||
from .util import (
|
||||
parse_requirement,
|
||||
cached_property,
|
||||
parse_name_and_version,
|
||||
read_exports,
|
||||
write_exports,
|
||||
CSVReader,
|
||||
CSVWriter,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Distribution', 'BaseInstalledDistribution', 'InstalledDistribution', 'EggInfoDistribution', 'DistributionPath'
|
||||
"Distribution",
|
||||
"BaseInstalledDistribution",
|
||||
"InstalledDistribution",
|
||||
"EggInfoDistribution",
|
||||
"DistributionPath",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXPORTS_FILENAME = 'pydist-exports.json'
|
||||
COMMANDS_FILENAME = 'pydist-commands.json'
|
||||
EXPORTS_FILENAME = "pydist-exports.json"
|
||||
COMMANDS_FILENAME = "pydist-commands.json"
|
||||
|
||||
DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED', 'RESOURCES', EXPORTS_FILENAME, 'SHARED')
|
||||
DIST_FILES = (
|
||||
"INSTALLER",
|
||||
METADATA_FILENAME,
|
||||
"RECORD",
|
||||
"REQUESTED",
|
||||
"RESOURCES",
|
||||
EXPORTS_FILENAME,
|
||||
"SHARED",
|
||||
)
|
||||
|
||||
DISTINFO_EXT = '.dist-info'
|
||||
DISTINFO_EXT = ".dist-info"
|
||||
|
||||
|
||||
class _Cache(object):
|
||||
@ -92,7 +116,7 @@ class DistributionPath(object):
|
||||
self._cache = _Cache()
|
||||
self._cache_egg = _Cache()
|
||||
self._cache_enabled = True
|
||||
self._scheme = get_scheme('default')
|
||||
self._scheme = get_scheme("default")
|
||||
|
||||
def _get_cache_enabled(self):
|
||||
return self._cache_enabled
|
||||
@ -121,7 +145,7 @@ class DistributionPath(object):
|
||||
finder = resources.finder_for_path(path)
|
||||
if finder is None:
|
||||
continue
|
||||
r = finder.find('')
|
||||
r = finder.find("")
|
||||
if not r or not r.is_container:
|
||||
continue
|
||||
rset = sorted(r.resources)
|
||||
@ -131,7 +155,11 @@ class DistributionPath(object):
|
||||
continue
|
||||
try:
|
||||
if self._include_dist and entry.endswith(DISTINFO_EXT):
|
||||
possible_filenames = [METADATA_FILENAME, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME]
|
||||
possible_filenames = [
|
||||
METADATA_FILENAME,
|
||||
WHEEL_METADATA_FILENAME,
|
||||
LEGACY_METADATA_FILENAME,
|
||||
]
|
||||
for metadata_filename in possible_filenames:
|
||||
metadata_path = posixpath.join(entry, metadata_filename)
|
||||
pydist = finder.find(metadata_path)
|
||||
@ -141,18 +169,19 @@ class DistributionPath(object):
|
||||
continue
|
||||
|
||||
with contextlib.closing(pydist.as_stream()) as stream:
|
||||
metadata = Metadata(fileobj=stream, scheme='legacy')
|
||||
logger.debug('Found %s', r.path)
|
||||
metadata = Metadata(fileobj=stream, scheme="legacy")
|
||||
logger.debug("Found %s", r.path)
|
||||
seen.add(r.path)
|
||||
yield new_dist_class(r.path, metadata=metadata, env=self)
|
||||
elif self._include_egg and entry.endswith(('.egg-info', '.egg')):
|
||||
logger.debug('Found %s', r.path)
|
||||
elif self._include_egg and entry.endswith((".egg-info", ".egg")):
|
||||
logger.debug("Found %s", r.path)
|
||||
seen.add(r.path)
|
||||
yield old_dist_class(r.path, self)
|
||||
except Exception as e:
|
||||
msg = 'Unable to read distribution at %s, perhaps due to bad metadata: %s'
|
||||
msg = "Unable to read distribution at %s, perhaps due to bad metadata: %s"
|
||||
logger.warning(msg, r.path, e)
|
||||
import warnings
|
||||
|
||||
warnings.warn(msg % (r.path, e), stacklevel=2)
|
||||
|
||||
def _generate_cache(self):
|
||||
@ -193,8 +222,8 @@ class DistributionPath(object):
|
||||
:type version: string
|
||||
:returns: directory name
|
||||
:rtype: string"""
|
||||
name = name.replace('-', '_')
|
||||
return '-'.join([name, version]) + DISTINFO_EXT
|
||||
name = name.replace("-", "_")
|
||||
return "-".join([name, version]) + DISTINFO_EXT
|
||||
|
||||
def get_distributions(self):
|
||||
"""
|
||||
@ -261,14 +290,16 @@ class DistributionPath(object):
|
||||
matcher = None
|
||||
if version is not None:
|
||||
try:
|
||||
matcher = self._scheme.matcher('%s (%s)' % (name, version))
|
||||
matcher = self._scheme.matcher("%s (%s)" % (name, version))
|
||||
except ValueError:
|
||||
raise DistlibException('invalid name or version: %r, %r' % (name, version))
|
||||
raise DistlibException(
|
||||
"invalid name or version: %r, %r" % (name, version)
|
||||
)
|
||||
|
||||
for dist in self.get_distributions():
|
||||
# We hit a problem on Travis where enum34 was installed and doesn't
|
||||
# have a provides attribute ...
|
||||
if not hasattr(dist, 'provides'):
|
||||
if not hasattr(dist, "provides"):
|
||||
logger.debug('No "provides": %s', dist)
|
||||
else:
|
||||
provided = dist.provides
|
||||
@ -290,7 +321,7 @@ class DistributionPath(object):
|
||||
"""
|
||||
dist = self.get_distribution(name)
|
||||
if dist is None:
|
||||
raise LookupError('no distribution named %r found' % name)
|
||||
raise LookupError("no distribution named %r found" % name)
|
||||
return dist.get_resource_path(relative_path)
|
||||
|
||||
def get_exported_entries(self, category, name=None):
|
||||
@ -361,7 +392,7 @@ class Distribution(object):
|
||||
"""
|
||||
A utility property which displays the name and version in parentheses.
|
||||
"""
|
||||
return '%s (%s)' % (self.name, self.version)
|
||||
return "%s (%s)" % (self.name, self.version)
|
||||
|
||||
@property
|
||||
def provides(self):
|
||||
@ -370,7 +401,7 @@ class Distribution(object):
|
||||
:return: A set of "name (version)" strings.
|
||||
"""
|
||||
plist = self.metadata.provides
|
||||
s = '%s (%s)' % (self.name, self.version)
|
||||
s = "%s (%s)" % (self.name, self.version)
|
||||
if s not in plist:
|
||||
plist.append(s)
|
||||
return plist
|
||||
@ -378,28 +409,30 @@ class Distribution(object):
|
||||
def _get_requirements(self, req_attr):
|
||||
md = self.metadata
|
||||
reqts = getattr(md, req_attr)
|
||||
logger.debug('%s: got requirements %r from metadata: %r', self.name, req_attr, reqts)
|
||||
logger.debug(
|
||||
"%s: got requirements %r from metadata: %r", self.name, req_attr, reqts
|
||||
)
|
||||
return set(md.get_requirements(reqts, extras=self.extras, env=self.context))
|
||||
|
||||
@property
|
||||
def run_requires(self):
|
||||
return self._get_requirements('run_requires')
|
||||
return self._get_requirements("run_requires")
|
||||
|
||||
@property
|
||||
def meta_requires(self):
|
||||
return self._get_requirements('meta_requires')
|
||||
return self._get_requirements("meta_requires")
|
||||
|
||||
@property
|
||||
def build_requires(self):
|
||||
return self._get_requirements('build_requires')
|
||||
return self._get_requirements("build_requires")
|
||||
|
||||
@property
|
||||
def test_requires(self):
|
||||
return self._get_requirements('test_requires')
|
||||
return self._get_requirements("test_requires")
|
||||
|
||||
@property
|
||||
def dev_requires(self):
|
||||
return self._get_requirements('dev_requires')
|
||||
return self._get_requirements("dev_requires")
|
||||
|
||||
def matches_requirement(self, req):
|
||||
"""
|
||||
@ -416,7 +449,7 @@ class Distribution(object):
|
||||
matcher = scheme.matcher(r.requirement)
|
||||
except UnsupportedVersionError:
|
||||
# XXX compat-mode if cannot read the version
|
||||
logger.warning('could not read version %r - using name only', req)
|
||||
logger.warning("could not read version %r - using name only", req)
|
||||
name = req.split()[0]
|
||||
matcher = scheme.matcher(name)
|
||||
|
||||
@ -439,10 +472,10 @@ class Distribution(object):
|
||||
Return a textual representation of this instance,
|
||||
"""
|
||||
if self.source_url:
|
||||
suffix = ' [%s]' % self.source_url
|
||||
suffix = " [%s]" % self.source_url
|
||||
else:
|
||||
suffix = ''
|
||||
return '<Distribution %s (%s)%s>' % (self.name, self.version, suffix)
|
||||
suffix = ""
|
||||
return "<Distribution %s (%s)%s>" % (self.name, self.version, suffix)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
@ -455,7 +488,11 @@ class Distribution(object):
|
||||
if type(other) is not type(self):
|
||||
result = False
|
||||
else:
|
||||
result = (self.name == other.name and self.version == other.version and self.source_url == other.source_url)
|
||||
result = (
|
||||
self.name == other.name
|
||||
and self.version == other.version
|
||||
and self.source_url == other.source_url
|
||||
)
|
||||
return result
|
||||
|
||||
def __hash__(self):
|
||||
@ -511,13 +548,13 @@ class BaseInstalledDistribution(Distribution):
|
||||
hasher = self.hasher
|
||||
if hasher is None:
|
||||
hasher = hashlib.md5
|
||||
prefix = ''
|
||||
prefix = ""
|
||||
else:
|
||||
hasher = getattr(hashlib, hasher)
|
||||
prefix = '%s=' % self.hasher
|
||||
prefix = "%s=" % self.hasher
|
||||
digest = hasher(data).digest()
|
||||
digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
|
||||
return '%s%s' % (prefix, digest)
|
||||
digest = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
||||
return "%s%s" % (prefix, digest)
|
||||
|
||||
|
||||
class InstalledDistribution(BaseInstalledDistribution):
|
||||
@ -528,13 +565,13 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
dry-run mode is being used).
|
||||
"""
|
||||
|
||||
hasher = 'sha256'
|
||||
hasher = "sha256"
|
||||
|
||||
def __init__(self, path, metadata=None, env=None):
|
||||
self.modules = []
|
||||
self.finder = finder = resources.finder_for_path(path)
|
||||
if finder is None:
|
||||
raise ValueError('finder unavailable for %s' % path)
|
||||
raise ValueError("finder unavailable for %s" % path)
|
||||
if env and env._cache_enabled and path in env._cache.path:
|
||||
metadata = env._cache.path[path].metadata
|
||||
elif metadata is None:
|
||||
@ -546,25 +583,29 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
if r is None:
|
||||
r = finder.find(LEGACY_METADATA_FILENAME)
|
||||
if r is None:
|
||||
raise ValueError('no %s found in %s' % (METADATA_FILENAME, path))
|
||||
raise ValueError("no %s found in %s" % (METADATA_FILENAME, path))
|
||||
with contextlib.closing(r.as_stream()) as stream:
|
||||
metadata = Metadata(fileobj=stream, scheme='legacy')
|
||||
metadata = Metadata(fileobj=stream, scheme="legacy")
|
||||
|
||||
super(InstalledDistribution, self).__init__(metadata, path, env)
|
||||
|
||||
if env and env._cache_enabled:
|
||||
env._cache.add(self)
|
||||
|
||||
r = finder.find('REQUESTED')
|
||||
r = finder.find("REQUESTED")
|
||||
self.requested = r is not None
|
||||
p = os.path.join(path, 'top_level.txt')
|
||||
p = os.path.join(path, "top_level.txt")
|
||||
if os.path.exists(p):
|
||||
with open(p, 'rb') as f:
|
||||
data = f.read().decode('utf-8')
|
||||
with open(p, "rb") as f:
|
||||
data = f.read().decode("utf-8")
|
||||
self.modules = data.splitlines()
|
||||
|
||||
def __repr__(self):
|
||||
return '<InstalledDistribution %r %s at %r>' % (self.name, self.version, self.path)
|
||||
return "<InstalledDistribution %r %s at %r>" % (
|
||||
self.name,
|
||||
self.version,
|
||||
self.path,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self.name, self.version)
|
||||
@ -577,7 +618,7 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
as stored in the file (which is as in PEP 376).
|
||||
"""
|
||||
results = []
|
||||
r = self.get_distinfo_resource('RECORD')
|
||||
r = self.get_distinfo_resource("RECORD")
|
||||
with contextlib.closing(r.as_stream()) as stream:
|
||||
with CSVReader(stream=stream) as record_reader:
|
||||
# Base location is parent dir of .dist-info dir
|
||||
@ -629,7 +670,7 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
individual export entries.
|
||||
"""
|
||||
rf = self.get_distinfo_file(EXPORTS_FILENAME)
|
||||
with open(rf, 'w') as f:
|
||||
with open(rf, "w") as f:
|
||||
write_exports(exports, f)
|
||||
|
||||
def get_resource_path(self, relative_path):
|
||||
@ -643,14 +684,15 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
of interest.
|
||||
:return: The absolute path where the resource is to be found.
|
||||
"""
|
||||
r = self.get_distinfo_resource('RESOURCES')
|
||||
r = self.get_distinfo_resource("RESOURCES")
|
||||
with contextlib.closing(r.as_stream()) as stream:
|
||||
with CSVReader(stream=stream) as resources_reader:
|
||||
for relative, destination in resources_reader:
|
||||
if relative == relative_path:
|
||||
return destination
|
||||
raise KeyError('no resource file with relative path %r '
|
||||
'is installed' % relative_path)
|
||||
raise KeyError(
|
||||
"no resource file with relative path %r " "is installed" % relative_path
|
||||
)
|
||||
|
||||
def list_installed_files(self):
|
||||
"""
|
||||
@ -669,31 +711,33 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
|
||||
prefix is used to determine when to write absolute paths.
|
||||
"""
|
||||
prefix = os.path.join(prefix, '')
|
||||
prefix = os.path.join(prefix, "")
|
||||
base = os.path.dirname(self.path)
|
||||
base_under_prefix = base.startswith(prefix)
|
||||
base = os.path.join(base, '')
|
||||
record_path = self.get_distinfo_file('RECORD')
|
||||
logger.info('creating %s', record_path)
|
||||
base = os.path.join(base, "")
|
||||
record_path = self.get_distinfo_file("RECORD")
|
||||
logger.info("creating %s", record_path)
|
||||
if dry_run:
|
||||
return None
|
||||
with CSVWriter(record_path) as writer:
|
||||
for path in paths:
|
||||
if os.path.isdir(path) or path.endswith(('.pyc', '.pyo')):
|
||||
if os.path.isdir(path) or path.endswith((".pyc", ".pyo")):
|
||||
# do not put size and hash, as in PEP-376
|
||||
hash_value = size = ''
|
||||
hash_value = size = ""
|
||||
else:
|
||||
size = '%d' % os.path.getsize(path)
|
||||
with open(path, 'rb') as fp:
|
||||
size = "%d" % os.path.getsize(path)
|
||||
with open(path, "rb") as fp:
|
||||
hash_value = self.get_hash(fp.read())
|
||||
if path.startswith(base) or (base_under_prefix and path.startswith(prefix)):
|
||||
if path.startswith(base) or (
|
||||
base_under_prefix and path.startswith(prefix)
|
||||
):
|
||||
path = os.path.relpath(path, base)
|
||||
writer.writerow((path, hash_value, size))
|
||||
|
||||
# add the RECORD file itself
|
||||
if record_path.startswith(base):
|
||||
record_path = os.path.relpath(record_path, base)
|
||||
writer.writerow((record_path, '', ''))
|
||||
writer.writerow((record_path, "", ""))
|
||||
return record_path
|
||||
|
||||
def check_installed_files(self):
|
||||
@ -707,28 +751,28 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
"""
|
||||
mismatches = []
|
||||
base = os.path.dirname(self.path)
|
||||
record_path = self.get_distinfo_file('RECORD')
|
||||
record_path = self.get_distinfo_file("RECORD")
|
||||
for path, hash_value, size in self.list_installed_files():
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(base, path)
|
||||
if path == record_path:
|
||||
continue
|
||||
if not os.path.exists(path):
|
||||
mismatches.append((path, 'exists', True, False))
|
||||
mismatches.append((path, "exists", True, False))
|
||||
elif os.path.isfile(path):
|
||||
actual_size = str(os.path.getsize(path))
|
||||
if size and actual_size != size:
|
||||
mismatches.append((path, 'size', size, actual_size))
|
||||
mismatches.append((path, "size", size, actual_size))
|
||||
elif hash_value:
|
||||
if '=' in hash_value:
|
||||
hasher = hash_value.split('=', 1)[0]
|
||||
if "=" in hash_value:
|
||||
hasher = hash_value.split("=", 1)[0]
|
||||
else:
|
||||
hasher = None
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
with open(path, "rb") as f:
|
||||
actual_hash = self.get_hash(f.read(), hasher)
|
||||
if actual_hash != hash_value:
|
||||
mismatches.append((path, 'hash', hash_value, actual_hash))
|
||||
mismatches.append((path, "hash", hash_value, actual_hash))
|
||||
return mismatches
|
||||
|
||||
@cached_property
|
||||
@ -746,13 +790,13 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
read from the SHARED file in the .dist-info directory.
|
||||
"""
|
||||
result = {}
|
||||
shared_path = os.path.join(self.path, 'SHARED')
|
||||
shared_path = os.path.join(self.path, "SHARED")
|
||||
if os.path.isfile(shared_path):
|
||||
with codecs.open(shared_path, 'r', encoding='utf-8') as f:
|
||||
with codecs.open(shared_path, "r", encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
for line in lines:
|
||||
key, value = line.split('=', 1)
|
||||
if key == 'namespace':
|
||||
key, value = line.split("=", 1)
|
||||
if key == "namespace":
|
||||
result.setdefault(key, []).append(value)
|
||||
else:
|
||||
result[key] = value
|
||||
@ -767,29 +811,30 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
written.
|
||||
:return: The path of the file written to.
|
||||
"""
|
||||
shared_path = os.path.join(self.path, 'SHARED')
|
||||
logger.info('creating %s', shared_path)
|
||||
shared_path = os.path.join(self.path, "SHARED")
|
||||
logger.info("creating %s", shared_path)
|
||||
if dry_run:
|
||||
return None
|
||||
lines = []
|
||||
for key in ('prefix', 'lib', 'headers', 'scripts', 'data'):
|
||||
for key in ("prefix", "lib", "headers", "scripts", "data"):
|
||||
path = paths[key]
|
||||
if os.path.isdir(paths[key]):
|
||||
lines.append('%s=%s' % (key, path))
|
||||
for ns in paths.get('namespace', ()):
|
||||
lines.append('namespace=%s' % ns)
|
||||
lines.append("%s=%s" % (key, path))
|
||||
for ns in paths.get("namespace", ()):
|
||||
lines.append("namespace=%s" % ns)
|
||||
|
||||
with codecs.open(shared_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
with codecs.open(shared_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
return shared_path
|
||||
|
||||
def get_distinfo_resource(self, path):
|
||||
if path not in DIST_FILES:
|
||||
raise DistlibException('invalid path for a dist-info file: '
|
||||
'%r at %r' % (path, self.path))
|
||||
raise DistlibException(
|
||||
"invalid path for a dist-info file: " "%r at %r" % (path, self.path)
|
||||
)
|
||||
finder = resources.finder_for_path(self.path)
|
||||
if finder is None:
|
||||
raise DistlibException('Unable to get a finder for %s' % self.path)
|
||||
raise DistlibException("Unable to get a finder for %s" % self.path)
|
||||
return finder.find(path)
|
||||
|
||||
def get_distinfo_file(self, path):
|
||||
@ -810,13 +855,16 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
# it's an absolute path?
|
||||
distinfo_dirname, path = path.split(os.sep)[-2:]
|
||||
if distinfo_dirname != self.path.split(os.sep)[-1]:
|
||||
raise DistlibException('dist-info file %r does not belong to the %r %s '
|
||||
'distribution' % (path, self.name, self.version))
|
||||
raise DistlibException(
|
||||
"dist-info file %r does not belong to the %r %s "
|
||||
"distribution" % (path, self.name, self.version)
|
||||
)
|
||||
|
||||
# The file must be relative
|
||||
if path not in DIST_FILES:
|
||||
raise DistlibException('invalid path for a dist-info file: '
|
||||
'%r at %r' % (path, self.path))
|
||||
raise DistlibException(
|
||||
"invalid path for a dist-info file: " "%r at %r" % (path, self.path)
|
||||
)
|
||||
|
||||
return os.path.join(self.path, path)
|
||||
|
||||
@ -837,7 +885,7 @@ class InstalledDistribution(BaseInstalledDistribution):
|
||||
yield path
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, InstalledDistribution) and self.path == other.path)
|
||||
return isinstance(other, InstalledDistribution) and self.path == other.path
|
||||
|
||||
# See http://docs.python.org/reference/datamodel#object.__hash__
|
||||
__hash__ = object.__hash__
|
||||
@ -889,21 +937,24 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
# sectioned files have bare newlines (separating sections)
|
||||
if not line: # pragma: no cover
|
||||
continue
|
||||
if line.startswith('['): # pragma: no cover
|
||||
logger.warning('Unexpected line: quitting requirement scan: %r', line)
|
||||
if line.startswith("["): # pragma: no cover
|
||||
logger.warning(
|
||||
"Unexpected line: quitting requirement scan: %r", line
|
||||
)
|
||||
break
|
||||
r = parse_requirement(line)
|
||||
if not r: # pragma: no cover
|
||||
logger.warning('Not recognised as a requirement: %r', line)
|
||||
logger.warning("Not recognised as a requirement: %r", line)
|
||||
continue
|
||||
if r.extras: # pragma: no cover
|
||||
logger.warning('extra requirements in requires.txt are '
|
||||
'not supported')
|
||||
logger.warning(
|
||||
"extra requirements in requires.txt are " "not supported"
|
||||
)
|
||||
if not r.constraints:
|
||||
reqs.append(r.name)
|
||||
else:
|
||||
cons = ', '.join('%s%s' % c for c in r.constraints)
|
||||
reqs.append('%s (%s)' % (r.name, cons))
|
||||
cons = ", ".join("%s%s" % c for c in r.constraints)
|
||||
reqs.append("%s (%s)" % (r.name, cons))
|
||||
return reqs
|
||||
|
||||
def parse_requires_path(req_path):
|
||||
@ -914,50 +965,51 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
|
||||
reqs = []
|
||||
try:
|
||||
with codecs.open(req_path, 'r', 'utf-8') as fp:
|
||||
with codecs.open(req_path, "r", "utf-8") as fp:
|
||||
reqs = parse_requires_data(fp.read())
|
||||
except IOError:
|
||||
pass
|
||||
return reqs
|
||||
|
||||
tl_path = tl_data = None
|
||||
if path.endswith('.egg'):
|
||||
if path.endswith(".egg"):
|
||||
if os.path.isdir(path):
|
||||
p = os.path.join(path, 'EGG-INFO')
|
||||
meta_path = os.path.join(p, 'PKG-INFO')
|
||||
metadata = Metadata(path=meta_path, scheme='legacy')
|
||||
req_path = os.path.join(p, 'requires.txt')
|
||||
tl_path = os.path.join(p, 'top_level.txt')
|
||||
p = os.path.join(path, "EGG-INFO")
|
||||
meta_path = os.path.join(p, "PKG-INFO")
|
||||
metadata = Metadata(path=meta_path, scheme="legacy")
|
||||
req_path = os.path.join(p, "requires.txt")
|
||||
tl_path = os.path.join(p, "top_level.txt")
|
||||
requires = parse_requires_path(req_path)
|
||||
else:
|
||||
# FIXME handle the case where zipfile is not available
|
||||
zipf = zipimport.zipimporter(path)
|
||||
fileobj = StringIO(zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8'))
|
||||
metadata = Metadata(fileobj=fileobj, scheme='legacy')
|
||||
fileobj = StringIO(zipf.get_data("EGG-INFO/PKG-INFO").decode("utf8"))
|
||||
metadata = Metadata(fileobj=fileobj, scheme="legacy")
|
||||
try:
|
||||
data = zipf.get_data('EGG-INFO/requires.txt')
|
||||
tl_data = zipf.get_data('EGG-INFO/top_level.txt').decode('utf-8')
|
||||
requires = parse_requires_data(data.decode('utf-8'))
|
||||
data = zipf.get_data("EGG-INFO/requires.txt")
|
||||
tl_data = zipf.get_data("EGG-INFO/top_level.txt").decode("utf-8")
|
||||
requires = parse_requires_data(data.decode("utf-8"))
|
||||
except IOError:
|
||||
requires = None
|
||||
elif path.endswith('.egg-info'):
|
||||
elif path.endswith(".egg-info"):
|
||||
if os.path.isdir(path):
|
||||
req_path = os.path.join(path, 'requires.txt')
|
||||
req_path = os.path.join(path, "requires.txt")
|
||||
requires = parse_requires_path(req_path)
|
||||
path = os.path.join(path, 'PKG-INFO')
|
||||
tl_path = os.path.join(path, 'top_level.txt')
|
||||
metadata = Metadata(path=path, scheme='legacy')
|
||||
path = os.path.join(path, "PKG-INFO")
|
||||
tl_path = os.path.join(path, "top_level.txt")
|
||||
metadata = Metadata(path=path, scheme="legacy")
|
||||
else:
|
||||
raise DistlibException('path must end with .egg-info or .egg, '
|
||||
'got %r' % path)
|
||||
raise DistlibException(
|
||||
"path must end with .egg-info or .egg, " "got %r" % path
|
||||
)
|
||||
|
||||
if requires:
|
||||
metadata.add_requirements(requires)
|
||||
# look for top-level modules in top_level.txt, if present
|
||||
if tl_data is None:
|
||||
if tl_path is not None and os.path.exists(tl_path):
|
||||
with open(tl_path, 'rb') as f:
|
||||
tl_data = f.read().decode('utf-8')
|
||||
with open(tl_path, "rb") as f:
|
||||
tl_data = f.read().decode("utf-8")
|
||||
if not tl_data:
|
||||
tl_data = []
|
||||
else:
|
||||
@ -966,7 +1018,11 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
return metadata
|
||||
|
||||
def __repr__(self):
|
||||
return '<EggInfoDistribution %r %s at %r>' % (self.name, self.version, self.path)
|
||||
return "<EggInfoDistribution %r %s at %r>" % (
|
||||
self.name,
|
||||
self.version,
|
||||
self.path,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self.name, self.version)
|
||||
@ -981,13 +1037,13 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
value and the actual value.
|
||||
"""
|
||||
mismatches = []
|
||||
record_path = os.path.join(self.path, 'installed-files.txt')
|
||||
record_path = os.path.join(self.path, "installed-files.txt")
|
||||
if os.path.exists(record_path):
|
||||
for path, _, _ in self.list_installed_files():
|
||||
if path == record_path:
|
||||
continue
|
||||
if not os.path.exists(path):
|
||||
mismatches.append((path, 'exists', True, False))
|
||||
mismatches.append((path, "exists", True, False))
|
||||
return mismatches
|
||||
|
||||
def list_installed_files(self):
|
||||
@ -999,7 +1055,7 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
"""
|
||||
|
||||
def _md5(path):
|
||||
f = open(path, 'rb')
|
||||
f = open(path, "rb")
|
||||
try:
|
||||
content = f.read()
|
||||
finally:
|
||||
@ -1009,18 +1065,18 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
def _size(path):
|
||||
return os.stat(path).st_size
|
||||
|
||||
record_path = os.path.join(self.path, 'installed-files.txt')
|
||||
record_path = os.path.join(self.path, "installed-files.txt")
|
||||
result = []
|
||||
if os.path.exists(record_path):
|
||||
with codecs.open(record_path, 'r', encoding='utf-8') as f:
|
||||
with codecs.open(record_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
p = os.path.normpath(os.path.join(self.path, line))
|
||||
# "./" is present as a marker between installed files
|
||||
# and installation metadata files
|
||||
if not os.path.exists(p):
|
||||
logger.warning('Non-existent file: %s', p)
|
||||
if p.endswith(('.pyc', '.pyo')):
|
||||
logger.warning("Non-existent file: %s", p)
|
||||
if p.endswith((".pyc", ".pyo")):
|
||||
continue
|
||||
# otherwise fall through and fail
|
||||
if not os.path.isdir(p):
|
||||
@ -1040,13 +1096,13 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
:type absolute: boolean
|
||||
:returns: iterator of paths
|
||||
"""
|
||||
record_path = os.path.join(self.path, 'installed-files.txt')
|
||||
record_path = os.path.join(self.path, "installed-files.txt")
|
||||
if os.path.exists(record_path):
|
||||
skip = True
|
||||
with codecs.open(record_path, 'r', encoding='utf-8') as f:
|
||||
with codecs.open(record_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line == './':
|
||||
if line == "./":
|
||||
skip = False
|
||||
continue
|
||||
if not skip:
|
||||
@ -1058,7 +1114,7 @@ class EggInfoDistribution(BaseInstalledDistribution):
|
||||
yield line
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, EggInfoDistribution) and self.path == other.path)
|
||||
return isinstance(other, EggInfoDistribution) and self.path == other.path
|
||||
|
||||
# See http://docs.python.org/reference/datamodel#object.__hash__
|
||||
__hash__ = object.__hash__
|
||||
@ -1122,11 +1178,11 @@ class DependencyGraph(object):
|
||||
or :class:`distutils2.database.EggInfoDistribution`
|
||||
:type requirement: ``str``
|
||||
"""
|
||||
logger.debug('%s missing %r', distribution, requirement)
|
||||
logger.debug("%s missing %r", distribution, requirement)
|
||||
self.missing.setdefault(distribution, []).append(requirement)
|
||||
|
||||
def _repr_dist(self, dist):
|
||||
return '%s %s' % (dist.name, dist.version)
|
||||
return "%s %s" % (dist.name, dist.version)
|
||||
|
||||
def repr_node(self, dist, level=1):
|
||||
"""Prints only a subgraph"""
|
||||
@ -1134,12 +1190,12 @@ class DependencyGraph(object):
|
||||
for other, label in self.adjacency_list[dist]:
|
||||
dist = self._repr_dist(other)
|
||||
if label is not None:
|
||||
dist = '%s [%s]' % (dist, label)
|
||||
output.append(' ' * level + str(dist))
|
||||
dist = "%s [%s]" % (dist, label)
|
||||
output.append(" " * level + str(dist))
|
||||
suboutput = self.repr_node(other, level + 1)
|
||||
subs = suboutput.split('\n')
|
||||
subs = suboutput.split("\n")
|
||||
output.extend(subs[1:])
|
||||
return '\n'.join(output)
|
||||
return "\n".join(output)
|
||||
|
||||
def to_dot(self, f, skip_disconnected=True):
|
||||
"""Writes a DOT output for the graph to the provided file *f*.
|
||||
@ -1158,19 +1214,21 @@ class DependencyGraph(object):
|
||||
disconnected.append(dist)
|
||||
for other, label in adjs:
|
||||
if label is not None:
|
||||
f.write('"%s" -> "%s" [label="%s"]\n' % (dist.name, other.name, label))
|
||||
f.write(
|
||||
'"%s" -> "%s" [label="%s"]\n' % (dist.name, other.name, label)
|
||||
)
|
||||
else:
|
||||
f.write('"%s" -> "%s"\n' % (dist.name, other.name))
|
||||
if not skip_disconnected and len(disconnected) > 0:
|
||||
f.write('subgraph disconnected {\n')
|
||||
f.write("subgraph disconnected {\n")
|
||||
f.write('label = "Disconnected"\n')
|
||||
f.write('bgcolor = red\n')
|
||||
f.write("bgcolor = red\n")
|
||||
|
||||
for dist in disconnected:
|
||||
f.write('"%s"' % dist.name)
|
||||
f.write('\n')
|
||||
f.write('}\n')
|
||||
f.write('}\n')
|
||||
f.write("\n")
|
||||
f.write("}\n")
|
||||
f.write("}\n")
|
||||
|
||||
def topological_sort(self):
|
||||
"""
|
||||
@ -1198,7 +1256,10 @@ class DependencyGraph(object):
|
||||
# Remove from the adjacency list of others
|
||||
for k, v in alist.items():
|
||||
alist[k] = [(d, r) for d, r in v if d not in to_remove]
|
||||
logger.debug('Moving to result: %s', ['%s (%s)' % (d.name, d.version) for d in to_remove])
|
||||
logger.debug(
|
||||
"Moving to result: %s",
|
||||
["%s (%s)" % (d.name, d.version) for d in to_remove],
|
||||
)
|
||||
result.extend(to_remove)
|
||||
return result, list(alist.keys())
|
||||
|
||||
@ -1207,10 +1268,10 @@ class DependencyGraph(object):
|
||||
output = []
|
||||
for dist, adjs in self.adjacency_list.items():
|
||||
output.append(self.repr_node(dist))
|
||||
return '\n'.join(output)
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def make_graph(dists, scheme='default'):
|
||||
def make_graph(dists, scheme="default"):
|
||||
"""Makes a dependency graph from the given distributions.
|
||||
|
||||
:parameter dists: a list of distributions
|
||||
@ -1228,18 +1289,23 @@ def make_graph(dists, scheme='default'):
|
||||
|
||||
for p in dist.provides:
|
||||
name, version = parse_name_and_version(p)
|
||||
logger.debug('Add to provided: %s, %s, %s', name, version, dist)
|
||||
logger.debug("Add to provided: %s, %s, %s", name, version, dist)
|
||||
provided.setdefault(name, []).append((version, dist))
|
||||
|
||||
# now make the edges
|
||||
for dist in dists:
|
||||
requires = (dist.run_requires | dist.meta_requires | dist.build_requires | dist.dev_requires)
|
||||
requires = (
|
||||
dist.run_requires
|
||||
| dist.meta_requires
|
||||
| dist.build_requires
|
||||
| dist.dev_requires
|
||||
)
|
||||
for req in requires:
|
||||
try:
|
||||
matcher = scheme.matcher(req)
|
||||
except UnsupportedVersionError:
|
||||
# XXX compat-mode if cannot read the version
|
||||
logger.warning('could not read version %r - using name only', req)
|
||||
logger.warning("could not read version %r - using name only", req)
|
||||
name = req.split()[0]
|
||||
matcher = scheme.matcher(name)
|
||||
|
||||
@ -1270,8 +1336,9 @@ def get_dependent_dists(dists, dist):
|
||||
:param dist: a distribution, member of *dists* for which we are interested
|
||||
"""
|
||||
if dist not in dists:
|
||||
raise DistlibException('given distribution %r is not a member '
|
||||
'of the list' % dist.name)
|
||||
raise DistlibException(
|
||||
"given distribution %r is not a member " "of the list" % dist.name
|
||||
)
|
||||
graph = make_graph(dists)
|
||||
|
||||
dep = [dist] # dependent distributions
|
||||
@ -1297,8 +1364,9 @@ def get_required_dists(dists, dist):
|
||||
in finding the dependencies.
|
||||
"""
|
||||
if dist not in dists:
|
||||
raise DistlibException('given distribution %r is not a member '
|
||||
'of the list' % dist.name)
|
||||
raise DistlibException(
|
||||
"given distribution %r is not a member " "of the list" % dist.name
|
||||
)
|
||||
graph = make_graph(dists)
|
||||
|
||||
req = set() # required distributions
|
||||
@ -1321,9 +1389,9 @@ def make_dist(name, version, **kwargs):
|
||||
"""
|
||||
A convenience method for making a dist given just a name and version.
|
||||
"""
|
||||
summary = kwargs.pop('summary', 'Placeholder for summary')
|
||||
summary = kwargs.pop("summary", "Placeholder for summary")
|
||||
md = Metadata(**kwargs)
|
||||
md.name = name
|
||||
md.version = version
|
||||
md.summary = summary or 'Placeholder for summary'
|
||||
md.summary = summary or "Placeholder for summary"
|
||||
return Distribution(md)
|
||||
|
||||
@ -10,20 +10,27 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
from threading import Thread
|
||||
except ImportError: # pragma: no cover
|
||||
from dummy_threading import Thread
|
||||
|
||||
from . import DistlibException
|
||||
from .compat import (HTTPBasicAuthHandler, Request, HTTPPasswordMgr,
|
||||
urlparse, build_opener, string_types)
|
||||
from .compat import (
|
||||
HTTPBasicAuthHandler,
|
||||
Request,
|
||||
HTTPPasswordMgr,
|
||||
urlparse,
|
||||
build_opener,
|
||||
string_types,
|
||||
)
|
||||
from .util import zip_dir, ServerProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_INDEX = 'https://pypi.org/pypi'
|
||||
DEFAULT_REALM = 'pypi'
|
||||
DEFAULT_INDEX = "https://pypi.org/pypi"
|
||||
DEFAULT_REALM = "pypi"
|
||||
|
||||
|
||||
class PackageIndex(object):
|
||||
@ -32,7 +39,7 @@ class PackageIndex(object):
|
||||
Package Index.
|
||||
"""
|
||||
|
||||
boundary = b'----------ThIs_Is_tHe_distlib_index_bouNdaRY_$'
|
||||
boundary = b"----------ThIs_Is_tHe_distlib_index_bouNdaRY_$"
|
||||
|
||||
def __init__(self, url=None):
|
||||
"""
|
||||
@ -44,19 +51,20 @@ class PackageIndex(object):
|
||||
self.url = url or DEFAULT_INDEX
|
||||
self.read_configuration()
|
||||
scheme, netloc, path, params, query, frag = urlparse(self.url)
|
||||
if params or query or frag or scheme not in ('http', 'https'):
|
||||
raise DistlibException('invalid repository: %s' % self.url)
|
||||
if params or query or frag or scheme not in ("http", "https"):
|
||||
raise DistlibException("invalid repository: %s" % self.url)
|
||||
self.password_handler = None
|
||||
self.ssl_verifier = None
|
||||
self.gpg = None
|
||||
self.gpg_home = None
|
||||
with open(os.devnull, 'w') as sink:
|
||||
with open(os.devnull, "w") as sink:
|
||||
# Use gpg by default rather than gpg2, as gpg2 insists on
|
||||
# prompting for passwords
|
||||
for s in ('gpg', 'gpg2'):
|
||||
for s in ("gpg", "gpg2"):
|
||||
try:
|
||||
rc = subprocess.check_call([s, '--version'], stdout=sink,
|
||||
stderr=sink)
|
||||
rc = subprocess.check_call(
|
||||
[s, "--version"], stdout=sink, stderr=sink
|
||||
)
|
||||
if rc == 0:
|
||||
self.gpg = s
|
||||
break
|
||||
@ -69,6 +77,7 @@ class PackageIndex(object):
|
||||
:return: the command.
|
||||
"""
|
||||
from .util import _get_pypirc_command as cmd
|
||||
|
||||
return cmd()
|
||||
|
||||
def read_configuration(self):
|
||||
@ -78,11 +87,12 @@ class PackageIndex(object):
|
||||
configuration.
|
||||
"""
|
||||
from .util import _load_pypirc
|
||||
|
||||
cfg = _load_pypirc(self)
|
||||
self.username = cfg.get('username')
|
||||
self.password = cfg.get('password')
|
||||
self.realm = cfg.get('realm', 'pypi')
|
||||
self.url = cfg.get('repository', self.url)
|
||||
self.username = cfg.get("username")
|
||||
self.password = cfg.get("password")
|
||||
self.realm = cfg.get("realm", "pypi")
|
||||
self.url = cfg.get("repository", self.url)
|
||||
|
||||
def save_configuration(self):
|
||||
"""
|
||||
@ -91,6 +101,7 @@ class PackageIndex(object):
|
||||
"""
|
||||
self.check_credentials()
|
||||
from .util import _store_pypirc
|
||||
|
||||
_store_pypirc(self)
|
||||
|
||||
def check_credentials(self):
|
||||
@ -99,7 +110,7 @@ class PackageIndex(object):
|
||||
exception if not.
|
||||
"""
|
||||
if self.username is None or self.password is None:
|
||||
raise DistlibException('username and password must be set')
|
||||
raise DistlibException("username and password must be set")
|
||||
pm = HTTPPasswordMgr()
|
||||
_, netloc, _, _, _, _ = urlparse(self.url)
|
||||
pm.add_password(self.realm, netloc, self.username, self.password)
|
||||
@ -118,10 +129,10 @@ class PackageIndex(object):
|
||||
self.check_credentials()
|
||||
metadata.validate()
|
||||
d = metadata.todict()
|
||||
d[':action'] = 'verify'
|
||||
d[":action"] = "verify"
|
||||
request = self.encode_request(d.items(), [])
|
||||
self.send_request(request)
|
||||
d[':action'] = 'submit'
|
||||
d[":action"] = "submit"
|
||||
request = self.encode_request(d.items(), [])
|
||||
return self.send_request(request)
|
||||
|
||||
@ -138,12 +149,14 @@ class PackageIndex(object):
|
||||
s = stream.readline()
|
||||
if not s:
|
||||
break
|
||||
s = s.decode('utf-8').rstrip()
|
||||
s = s.decode("utf-8").rstrip()
|
||||
outbuf.append(s)
|
||||
logger.debug('%s: %s' % (name, s))
|
||||
logger.debug("%s: %s" % (name, s))
|
||||
stream.close()
|
||||
|
||||
def get_sign_command(self, filename, signer, sign_password, keystore=None): # pragma: no cover
|
||||
def get_sign_command(
|
||||
self, filename, signer, sign_password, keystore=None
|
||||
): # pragma: no cover
|
||||
"""
|
||||
Return a suitable command for signing a file.
|
||||
|
||||
@ -157,18 +170,27 @@ class PackageIndex(object):
|
||||
:return: The signing command as a list suitable to be
|
||||
passed to :class:`subprocess.Popen`.
|
||||
"""
|
||||
cmd = [self.gpg, '--status-fd', '2', '--no-tty']
|
||||
cmd = [self.gpg, "--status-fd", "2", "--no-tty"]
|
||||
if keystore is None:
|
||||
keystore = self.gpg_home
|
||||
if keystore:
|
||||
cmd.extend(['--homedir', keystore])
|
||||
cmd.extend(["--homedir", keystore])
|
||||
if sign_password is not None:
|
||||
cmd.extend(['--batch', '--passphrase-fd', '0'])
|
||||
cmd.extend(["--batch", "--passphrase-fd", "0"])
|
||||
td = tempfile.mkdtemp()
|
||||
sf = os.path.join(td, os.path.basename(filename) + '.asc')
|
||||
cmd.extend(['--detach-sign', '--armor', '--local-user',
|
||||
signer, '--output', sf, filename])
|
||||
logger.debug('invoking: %s', ' '.join(cmd))
|
||||
sf = os.path.join(td, os.path.basename(filename) + ".asc")
|
||||
cmd.extend(
|
||||
[
|
||||
"--detach-sign",
|
||||
"--armor",
|
||||
"--local-user",
|
||||
signer,
|
||||
"--output",
|
||||
sf,
|
||||
filename,
|
||||
]
|
||||
)
|
||||
logger.debug("invoking: %s", " ".join(cmd))
|
||||
return cmd, sf
|
||||
|
||||
def run_command(self, cmd, input_data=None):
|
||||
@ -183,19 +205,19 @@ class PackageIndex(object):
|
||||
lines read from the subprocess' ``stderr``.
|
||||
"""
|
||||
kwargs = {
|
||||
'stdout': subprocess.PIPE,
|
||||
'stderr': subprocess.PIPE,
|
||||
"stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.PIPE,
|
||||
}
|
||||
if input_data is not None:
|
||||
kwargs['stdin'] = subprocess.PIPE
|
||||
kwargs["stdin"] = subprocess.PIPE
|
||||
stdout = []
|
||||
stderr = []
|
||||
p = subprocess.Popen(cmd, **kwargs)
|
||||
# We don't use communicate() here because we may need to
|
||||
# get clever with interacting with the command
|
||||
t1 = Thread(target=self._reader, args=('stdout', p.stdout, stdout))
|
||||
t1 = Thread(target=self._reader, args=("stdout", p.stdout, stdout))
|
||||
t1.start()
|
||||
t2 = Thread(target=self._reader, args=('stderr', p.stderr, stderr))
|
||||
t2 = Thread(target=self._reader, args=("stderr", p.stderr, stderr))
|
||||
t2.start()
|
||||
if input_data is not None:
|
||||
p.stdin.write(input_data)
|
||||
@ -206,7 +228,9 @@ class PackageIndex(object):
|
||||
t2.join()
|
||||
return p.returncode, stdout, stderr
|
||||
|
||||
def sign_file(self, filename, signer, sign_password, keystore=None): # pragma: no cover
|
||||
def sign_file(
|
||||
self, filename, signer, sign_password, keystore=None
|
||||
): # pragma: no cover
|
||||
"""
|
||||
Sign a file.
|
||||
|
||||
@ -220,17 +244,22 @@ class PackageIndex(object):
|
||||
:return: The absolute pathname of the file where the signature is
|
||||
stored.
|
||||
"""
|
||||
cmd, sig_file = self.get_sign_command(filename, signer, sign_password,
|
||||
keystore)
|
||||
rc, stdout, stderr = self.run_command(cmd,
|
||||
sign_password.encode('utf-8'))
|
||||
cmd, sig_file = self.get_sign_command(filename, signer, sign_password, keystore)
|
||||
rc, stdout, stderr = self.run_command(cmd, sign_password.encode("utf-8"))
|
||||
if rc != 0:
|
||||
raise DistlibException('sign command failed with error '
|
||||
'code %s' % rc)
|
||||
raise DistlibException("sign command failed with error " "code %s" % rc)
|
||||
return sig_file
|
||||
|
||||
def upload_file(self, metadata, filename, signer=None, sign_password=None,
|
||||
filetype='sdist', pyversion='source', keystore=None):
|
||||
def upload_file(
|
||||
self,
|
||||
metadata,
|
||||
filename,
|
||||
signer=None,
|
||||
sign_password=None,
|
||||
filetype="sdist",
|
||||
pyversion="source",
|
||||
keystore=None,
|
||||
):
|
||||
"""
|
||||
Upload a release file to the index.
|
||||
|
||||
@ -254,34 +283,34 @@ class PackageIndex(object):
|
||||
"""
|
||||
self.check_credentials()
|
||||
if not os.path.exists(filename):
|
||||
raise DistlibException('not found: %s' % filename)
|
||||
raise DistlibException("not found: %s" % filename)
|
||||
metadata.validate()
|
||||
d = metadata.todict()
|
||||
sig_file = None
|
||||
if signer:
|
||||
if not self.gpg:
|
||||
logger.warning('no signing program available - not signed')
|
||||
logger.warning("no signing program available - not signed")
|
||||
else:
|
||||
sig_file = self.sign_file(filename, signer, sign_password,
|
||||
keystore)
|
||||
with open(filename, 'rb') as f:
|
||||
sig_file = self.sign_file(filename, signer, sign_password, keystore)
|
||||
with open(filename, "rb") as f:
|
||||
file_data = f.read()
|
||||
md5_digest = hashlib.md5(file_data).hexdigest()
|
||||
sha256_digest = hashlib.sha256(file_data).hexdigest()
|
||||
d.update({
|
||||
':action': 'file_upload',
|
||||
'protocol_version': '1',
|
||||
'filetype': filetype,
|
||||
'pyversion': pyversion,
|
||||
'md5_digest': md5_digest,
|
||||
'sha256_digest': sha256_digest,
|
||||
})
|
||||
files = [('content', os.path.basename(filename), file_data)]
|
||||
d.update(
|
||||
{
|
||||
":action": "file_upload",
|
||||
"protocol_version": "1",
|
||||
"filetype": filetype,
|
||||
"pyversion": pyversion,
|
||||
"md5_digest": md5_digest,
|
||||
"sha256_digest": sha256_digest,
|
||||
}
|
||||
)
|
||||
files = [("content", os.path.basename(filename), file_data)]
|
||||
if sig_file:
|
||||
with open(sig_file, 'rb') as f:
|
||||
with open(sig_file, "rb") as f:
|
||||
sig_data = f.read()
|
||||
files.append(('gpg_signature', os.path.basename(sig_file),
|
||||
sig_data))
|
||||
files.append(("gpg_signature", os.path.basename(sig_file), sig_data))
|
||||
shutil.rmtree(os.path.dirname(sig_file))
|
||||
request = self.encode_request(d.items(), files)
|
||||
return self.send_request(request)
|
||||
@ -301,21 +330,19 @@ class PackageIndex(object):
|
||||
"""
|
||||
self.check_credentials()
|
||||
if not os.path.isdir(doc_dir):
|
||||
raise DistlibException('not a directory: %r' % doc_dir)
|
||||
fn = os.path.join(doc_dir, 'index.html')
|
||||
raise DistlibException("not a directory: %r" % doc_dir)
|
||||
fn = os.path.join(doc_dir, "index.html")
|
||||
if not os.path.exists(fn):
|
||||
raise DistlibException('not found: %r' % fn)
|
||||
raise DistlibException("not found: %r" % fn)
|
||||
metadata.validate()
|
||||
name, version = metadata.name, metadata.version
|
||||
zip_data = zip_dir(doc_dir).getvalue()
|
||||
fields = [(':action', 'doc_upload'),
|
||||
('name', name), ('version', version)]
|
||||
files = [('content', name, zip_data)]
|
||||
fields = [(":action", "doc_upload"), ("name", name), ("version", version)]
|
||||
files = [("content", name, zip_data)]
|
||||
request = self.encode_request(fields, files)
|
||||
return self.send_request(request)
|
||||
|
||||
def get_verify_command(self, signature_filename, data_filename,
|
||||
keystore=None):
|
||||
def get_verify_command(self, signature_filename, data_filename, keystore=None):
|
||||
"""
|
||||
Return a suitable command for verifying a file.
|
||||
|
||||
@ -329,17 +356,16 @@ class PackageIndex(object):
|
||||
:return: The verifying command as a list suitable to be
|
||||
passed to :class:`subprocess.Popen`.
|
||||
"""
|
||||
cmd = [self.gpg, '--status-fd', '2', '--no-tty']
|
||||
cmd = [self.gpg, "--status-fd", "2", "--no-tty"]
|
||||
if keystore is None:
|
||||
keystore = self.gpg_home
|
||||
if keystore:
|
||||
cmd.extend(['--homedir', keystore])
|
||||
cmd.extend(['--verify', signature_filename, data_filename])
|
||||
logger.debug('invoking: %s', ' '.join(cmd))
|
||||
cmd.extend(["--homedir", keystore])
|
||||
cmd.extend(["--verify", signature_filename, data_filename])
|
||||
logger.debug("invoking: %s", " ".join(cmd))
|
||||
return cmd
|
||||
|
||||
def verify_signature(self, signature_filename, data_filename,
|
||||
keystore=None):
|
||||
def verify_signature(self, signature_filename, data_filename, keystore=None):
|
||||
"""
|
||||
Verify a signature for a file.
|
||||
|
||||
@ -353,13 +379,13 @@ class PackageIndex(object):
|
||||
:return: True if the signature was verified, else False.
|
||||
"""
|
||||
if not self.gpg:
|
||||
raise DistlibException('verification unavailable because gpg '
|
||||
'unavailable')
|
||||
cmd = self.get_verify_command(signature_filename, data_filename,
|
||||
keystore)
|
||||
raise DistlibException(
|
||||
"verification unavailable because gpg " "unavailable"
|
||||
)
|
||||
cmd = self.get_verify_command(signature_filename, data_filename, keystore)
|
||||
rc, stdout, stderr = self.run_command(cmd)
|
||||
if rc not in (0, 1):
|
||||
raise DistlibException('verify command failed with error code %s' % rc)
|
||||
raise DistlibException("verify command failed with error code %s" % rc)
|
||||
return rc == 0
|
||||
|
||||
def download_file(self, url, destfile, digest=None, reporthook=None):
|
||||
@ -386,18 +412,18 @@ class PackageIndex(object):
|
||||
"""
|
||||
if digest is None:
|
||||
digester = None
|
||||
logger.debug('No digest specified')
|
||||
logger.debug("No digest specified")
|
||||
else:
|
||||
if isinstance(digest, (list, tuple)):
|
||||
hasher, digest = digest
|
||||
else:
|
||||
hasher = 'md5'
|
||||
hasher = "md5"
|
||||
digester = getattr(hashlib, hasher)()
|
||||
logger.debug('Digest specified: %s' % digest)
|
||||
logger.debug("Digest specified: %s" % digest)
|
||||
# The following code is equivalent to urlretrieve.
|
||||
# We need to do it this way so that we can compute the
|
||||
# digest of the file as we go.
|
||||
with open(destfile, 'wb') as dfp:
|
||||
with open(destfile, "wb") as dfp:
|
||||
# addinfourl is not a context manager on 2.x
|
||||
# so we have to use try/finally
|
||||
sfp = self.send_request(Request(url))
|
||||
@ -428,16 +454,17 @@ class PackageIndex(object):
|
||||
# check that we got the whole file, if we can
|
||||
if size >= 0 and read < size:
|
||||
raise DistlibException(
|
||||
'retrieval incomplete: got only %d out of %d bytes'
|
||||
% (read, size))
|
||||
"retrieval incomplete: got only %d out of %d bytes" % (read, size)
|
||||
)
|
||||
# if we have a digest, it must match.
|
||||
if digester:
|
||||
actual = digester.hexdigest()
|
||||
if digest != actual:
|
||||
raise DistlibException('%s digest mismatch for %s: expected '
|
||||
'%s, got %s' % (hasher, destfile,
|
||||
digest, actual))
|
||||
logger.debug('Digest verified: %s', digest)
|
||||
raise DistlibException(
|
||||
"%s digest mismatch for %s: expected "
|
||||
"%s, got %s" % (hasher, destfile, digest, actual)
|
||||
)
|
||||
logger.debug("Digest verified: %s", digest)
|
||||
|
||||
def send_request(self, req):
|
||||
"""
|
||||
@ -474,35 +501,41 @@ class PackageIndex(object):
|
||||
values = [values]
|
||||
|
||||
for v in values:
|
||||
parts.extend((
|
||||
b'--' + boundary,
|
||||
('Content-Disposition: form-data; name="%s"' %
|
||||
k).encode('utf-8'),
|
||||
b'',
|
||||
v.encode('utf-8')))
|
||||
parts.extend(
|
||||
(
|
||||
b"--" + boundary,
|
||||
('Content-Disposition: form-data; name="%s"' % k).encode(
|
||||
"utf-8"
|
||||
),
|
||||
b"",
|
||||
v.encode("utf-8"),
|
||||
)
|
||||
)
|
||||
for key, filename, value in files:
|
||||
parts.extend((
|
||||
b'--' + boundary,
|
||||
('Content-Disposition: form-data; name="%s"; filename="%s"' %
|
||||
(key, filename)).encode('utf-8'),
|
||||
b'',
|
||||
value))
|
||||
parts.extend(
|
||||
(
|
||||
b"--" + boundary,
|
||||
(
|
||||
'Content-Disposition: form-data; name="%s"; filename="%s"'
|
||||
% (key, filename)
|
||||
).encode("utf-8"),
|
||||
b"",
|
||||
value,
|
||||
)
|
||||
)
|
||||
|
||||
parts.extend((b'--' + boundary + b'--', b''))
|
||||
parts.extend((b"--" + boundary + b"--", b""))
|
||||
|
||||
body = b'\r\n'.join(parts)
|
||||
ct = b'multipart/form-data; boundary=' + boundary
|
||||
headers = {
|
||||
'Content-type': ct,
|
||||
'Content-length': str(len(body))
|
||||
}
|
||||
body = b"\r\n".join(parts)
|
||||
ct = b"multipart/form-data; boundary=" + boundary
|
||||
headers = {"Content-type": ct, "Content-length": str(len(body))}
|
||||
return Request(self.url, body, headers)
|
||||
|
||||
def search(self, terms, operator=None): # pragma: no cover
|
||||
if isinstance(terms, string_types):
|
||||
terms = {'name': terms}
|
||||
terms = {"name": terms}
|
||||
rpc_proxy = ServerProxy(self.url, timeout=3.0)
|
||||
try:
|
||||
return rpc_proxy.search(terms, operator or 'and')
|
||||
return rpc_proxy.search(terms, operator or "and")
|
||||
finally:
|
||||
rpc_proxy('close')()
|
||||
rpc_proxy("close")()
|
||||
|
||||
@ -12,6 +12,7 @@ import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
|
||||
try:
|
||||
import threading
|
||||
except ImportError: # pragma: no cover
|
||||
@ -19,21 +20,43 @@ except ImportError: # pragma: no cover
|
||||
import zlib
|
||||
|
||||
from . import DistlibException
|
||||
from .compat import (urljoin, urlparse, urlunparse, url2pathname, pathname2url, queue, quote, unescape, build_opener,
|
||||
HTTPRedirectHandler as BaseRedirectHandler, text_type, Request, HTTPError, URLError)
|
||||
from .compat import (
|
||||
urljoin,
|
||||
urlparse,
|
||||
urlunparse,
|
||||
url2pathname,
|
||||
pathname2url,
|
||||
queue,
|
||||
quote,
|
||||
unescape,
|
||||
build_opener,
|
||||
HTTPRedirectHandler as BaseRedirectHandler,
|
||||
text_type,
|
||||
Request,
|
||||
HTTPError,
|
||||
URLError,
|
||||
)
|
||||
from .database import Distribution, DistributionPath, make_dist
|
||||
from .metadata import Metadata, MetadataInvalidError
|
||||
from .util import (cached_property, ensure_slash, split_filename, get_project_data, parse_requirement,
|
||||
parse_name_and_version, ServerProxy, normalize_name)
|
||||
from .util import (
|
||||
cached_property,
|
||||
ensure_slash,
|
||||
split_filename,
|
||||
get_project_data,
|
||||
parse_requirement,
|
||||
parse_name_and_version,
|
||||
ServerProxy,
|
||||
normalize_name,
|
||||
)
|
||||
from .version import get_scheme, UnsupportedVersionError
|
||||
from .wheel import Wheel, is_compatible
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HASHER_HASH = re.compile(r'^(\w+)=([a-f0-9]+)')
|
||||
CHARSET = re.compile(r';\s*charset\s*=\s*(.*)\s*$', re.I)
|
||||
HTML_CONTENT_TYPE = re.compile('text/html|application/x(ht)?ml')
|
||||
DEFAULT_INDEX = 'https://pypi.org/pypi'
|
||||
HASHER_HASH = re.compile(r"^(\w+)=([a-f0-9]+)")
|
||||
CHARSET = re.compile(r";\s*charset\s*=\s*(.*)\s*$", re.I)
|
||||
HTML_CONTENT_TYPE = re.compile("text/html|application/x(ht)?ml")
|
||||
DEFAULT_INDEX = "https://pypi.org/pypi"
|
||||
|
||||
|
||||
def get_all_distribution_names(url=None):
|
||||
@ -48,7 +71,7 @@ def get_all_distribution_names(url=None):
|
||||
try:
|
||||
return client.list_packages()
|
||||
finally:
|
||||
client('close')()
|
||||
client("close")()
|
||||
|
||||
|
||||
class RedirectHandler(BaseRedirectHandler):
|
||||
@ -65,16 +88,16 @@ class RedirectHandler(BaseRedirectHandler):
|
||||
# Some servers (incorrectly) return multiple Location headers
|
||||
# (so probably same goes for URI). Use first header.
|
||||
newurl = None
|
||||
for key in ('location', 'uri'):
|
||||
for key in ("location", "uri"):
|
||||
if key in headers:
|
||||
newurl = headers[key]
|
||||
break
|
||||
if newurl is None: # pragma: no cover
|
||||
return
|
||||
urlparts = urlparse(newurl)
|
||||
if urlparts.scheme == '':
|
||||
if urlparts.scheme == "":
|
||||
newurl = urljoin(req.get_full_url(), newurl)
|
||||
if hasattr(headers, 'replace_header'):
|
||||
if hasattr(headers, "replace_header"):
|
||||
headers.replace_header(key, newurl)
|
||||
else:
|
||||
headers[key] = newurl
|
||||
@ -87,9 +110,10 @@ class Locator(object):
|
||||
"""
|
||||
A base class for locators - things that locate distributions.
|
||||
"""
|
||||
source_extensions = ('.tar.gz', '.tar.bz2', '.tar', '.zip', '.tgz', '.tbz')
|
||||
binary_extensions = ('.egg', '.exe', '.whl')
|
||||
excluded_extensions = ('.pdf', )
|
||||
|
||||
source_extensions = (".tar.gz", ".tar.bz2", ".tar", ".zip", ".tgz", ".tbz")
|
||||
binary_extensions = (".egg", ".exe", ".whl")
|
||||
excluded_extensions = (".pdf",)
|
||||
|
||||
# A list of tags indicating which wheels you want to match. The default
|
||||
# value of None matches against the tags compatible with the running
|
||||
@ -97,9 +121,9 @@ class Locator(object):
|
||||
# instance to a list of tuples (pyver, abi, arch) which you want to match.
|
||||
wheel_tags = None
|
||||
|
||||
downloadable_extensions = source_extensions + ('.whl', )
|
||||
downloadable_extensions = source_extensions + (".whl",)
|
||||
|
||||
def __init__(self, scheme='default'):
|
||||
def __init__(self, scheme="default"):
|
||||
"""
|
||||
Initialise an instance.
|
||||
:param scheme: Because locators look for most recent versions, they
|
||||
@ -160,13 +184,13 @@ class Locator(object):
|
||||
If called from a locate() request, self.matcher will be set to a
|
||||
matcher for the requirement to satisfy, otherwise it will be None.
|
||||
"""
|
||||
raise NotImplementedError('Please implement in the subclass')
|
||||
raise NotImplementedError("Please implement in the subclass")
|
||||
|
||||
def get_distribution_names(self):
|
||||
"""
|
||||
Return all the distribution names known to this locator.
|
||||
"""
|
||||
raise NotImplementedError('Please implement in the subclass')
|
||||
raise NotImplementedError("Please implement in the subclass")
|
||||
|
||||
def get_project(self, name):
|
||||
"""
|
||||
@ -193,11 +217,18 @@ class Locator(object):
|
||||
t = urlparse(url)
|
||||
basename = posixpath.basename(t.path)
|
||||
compatible = True
|
||||
is_wheel = basename.endswith('.whl')
|
||||
is_wheel = basename.endswith(".whl")
|
||||
is_downloadable = basename.endswith(self.downloadable_extensions)
|
||||
if is_wheel:
|
||||
compatible = is_compatible(Wheel(basename), self.wheel_tags)
|
||||
return (t.scheme == 'https', 'pypi.org' in t.netloc, is_downloadable, is_wheel, compatible, basename)
|
||||
return (
|
||||
t.scheme == "https",
|
||||
"pypi.org" in t.netloc,
|
||||
is_downloadable,
|
||||
is_wheel,
|
||||
compatible,
|
||||
basename,
|
||||
)
|
||||
|
||||
def prefer_url(self, url1, url2):
|
||||
"""
|
||||
@ -216,9 +247,9 @@ class Locator(object):
|
||||
if s1 > s2:
|
||||
result = url1
|
||||
if result != url2:
|
||||
logger.debug('Not replacing %r with %r', url1, url2)
|
||||
logger.debug("Not replacing %r with %r", url1, url2)
|
||||
else:
|
||||
logger.debug('Replacing %r with %r', url1, url2)
|
||||
logger.debug("Replacing %r with %r", url1, url2)
|
||||
return result
|
||||
|
||||
def split_filename(self, filename, project_name):
|
||||
@ -241,21 +272,21 @@ class Locator(object):
|
||||
|
||||
result = None
|
||||
scheme, netloc, path, params, query, frag = urlparse(url)
|
||||
if frag.lower().startswith('egg='): # pragma: no cover
|
||||
logger.debug('%s: version hint in fragment: %r', project_name, frag)
|
||||
if frag.lower().startswith("egg="): # pragma: no cover
|
||||
logger.debug("%s: version hint in fragment: %r", project_name, frag)
|
||||
m = HASHER_HASH.match(frag)
|
||||
if m:
|
||||
algo, digest = m.groups()
|
||||
else:
|
||||
algo, digest = None, None
|
||||
origpath = path
|
||||
if path and path[-1] == '/': # pragma: no cover
|
||||
if path and path[-1] == "/": # pragma: no cover
|
||||
path = path[:-1]
|
||||
if path.endswith('.whl'):
|
||||
if path.endswith(".whl"):
|
||||
try:
|
||||
wheel = Wheel(path)
|
||||
if not is_compatible(wheel, self.wheel_tags):
|
||||
logger.debug('Wheel not compatible: %s', path)
|
||||
logger.debug("Wheel not compatible: %s", path)
|
||||
else:
|
||||
if project_name is None:
|
||||
include = True
|
||||
@ -263,38 +294,44 @@ class Locator(object):
|
||||
include = same_project(wheel.name, project_name)
|
||||
if include:
|
||||
result = {
|
||||
'name': wheel.name,
|
||||
'version': wheel.version,
|
||||
'filename': wheel.filename,
|
||||
'url': urlunparse((scheme, netloc, origpath, params, query, '')),
|
||||
'python-version': ', '.join(['.'.join(list(v[2:])) for v in wheel.pyver]),
|
||||
"name": wheel.name,
|
||||
"version": wheel.version,
|
||||
"filename": wheel.filename,
|
||||
"url": urlunparse(
|
||||
(scheme, netloc, origpath, params, query, "")
|
||||
),
|
||||
"python-version": ", ".join(
|
||||
[".".join(list(v[2:])) for v in wheel.pyver]
|
||||
),
|
||||
}
|
||||
except Exception: # pragma: no cover
|
||||
logger.warning('invalid path for wheel: %s', path)
|
||||
logger.warning("invalid path for wheel: %s", path)
|
||||
elif not path.endswith(self.downloadable_extensions): # pragma: no cover
|
||||
logger.debug('Not downloadable: %s', path)
|
||||
logger.debug("Not downloadable: %s", path)
|
||||
else: # downloadable extension
|
||||
path = filename = posixpath.basename(path)
|
||||
for ext in self.downloadable_extensions:
|
||||
if path.endswith(ext):
|
||||
path = path[:-len(ext)]
|
||||
path = path[: -len(ext)]
|
||||
t = self.split_filename(path, project_name)
|
||||
if not t: # pragma: no cover
|
||||
logger.debug('No match for project/version: %s', path)
|
||||
logger.debug("No match for project/version: %s", path)
|
||||
else:
|
||||
name, version, pyver = t
|
||||
if not project_name or same_project(project_name, name):
|
||||
result = {
|
||||
'name': name,
|
||||
'version': version,
|
||||
'filename': filename,
|
||||
'url': urlunparse((scheme, netloc, origpath, params, query, '')),
|
||||
"name": name,
|
||||
"version": version,
|
||||
"filename": filename,
|
||||
"url": urlunparse(
|
||||
(scheme, netloc, origpath, params, query, "")
|
||||
),
|
||||
}
|
||||
if pyver: # pragma: no cover
|
||||
result['python-version'] = pyver
|
||||
result["python-version"] = pyver
|
||||
break
|
||||
if result and algo:
|
||||
result['%s_digest' % algo] = digest
|
||||
result["%s_digest" % algo] = digest
|
||||
return result
|
||||
|
||||
def _get_digest(self, info):
|
||||
@ -306,15 +343,15 @@ class Locator(object):
|
||||
looks only for SHA256, then MD5.
|
||||
"""
|
||||
result = None
|
||||
if 'digests' in info:
|
||||
digests = info['digests']
|
||||
for algo in ('sha256', 'md5'):
|
||||
if "digests" in info:
|
||||
digests = info["digests"]
|
||||
for algo in ("sha256", "md5"):
|
||||
if algo in digests:
|
||||
result = (algo, digests[algo])
|
||||
break
|
||||
if not result:
|
||||
for algo in ('sha256', 'md5'):
|
||||
key = '%s_digest' % algo
|
||||
for algo in ("sha256", "md5"):
|
||||
key = "%s_digest" % algo
|
||||
if key in info:
|
||||
result = (algo, info[key])
|
||||
break
|
||||
@ -326,8 +363,8 @@ class Locator(object):
|
||||
dictionary for a specific version, which typically holds information
|
||||
gleaned from a filename or URL for an archive for the distribution.
|
||||
"""
|
||||
name = info.pop('name')
|
||||
version = info.pop('version')
|
||||
name = info.pop("name")
|
||||
version = info.pop("version")
|
||||
if version in result:
|
||||
dist = result[version]
|
||||
md = dist.metadata
|
||||
@ -335,11 +372,11 @@ class Locator(object):
|
||||
dist = make_dist(name, version, scheme=self.scheme)
|
||||
md = dist.metadata
|
||||
dist.digest = digest = self._get_digest(info)
|
||||
url = info['url']
|
||||
result['digests'][url] = digest
|
||||
if md.source_url != info['url']:
|
||||
url = info["url"]
|
||||
result["digests"][url] = digest
|
||||
if md.source_url != info["url"]:
|
||||
md.source_url = self.prefer_url(md.source_url, url)
|
||||
result['urls'].setdefault(version, set()).add(url)
|
||||
result["urls"].setdefault(version, set()).add(url)
|
||||
dist.locator = self
|
||||
result[version] = dist
|
||||
|
||||
@ -359,17 +396,17 @@ class Locator(object):
|
||||
result = None
|
||||
r = parse_requirement(requirement)
|
||||
if r is None: # pragma: no cover
|
||||
raise DistlibException('Not a valid requirement: %r' % requirement)
|
||||
raise DistlibException("Not a valid requirement: %r" % requirement)
|
||||
scheme = get_scheme(self.scheme)
|
||||
self.matcher = matcher = scheme.matcher(r.requirement)
|
||||
logger.debug('matcher: %s (%s)', matcher, type(matcher).__name__)
|
||||
logger.debug("matcher: %s (%s)", matcher, type(matcher).__name__)
|
||||
versions = self.get_project(r.name)
|
||||
if len(versions) > 2: # urls and digests keys are present
|
||||
# sometimes, versions are invalid
|
||||
slist = []
|
||||
vcls = matcher.version_class
|
||||
for k in versions:
|
||||
if k in ('urls', 'digests'):
|
||||
if k in ("urls", "digests"):
|
||||
continue
|
||||
try:
|
||||
if not matcher.match(k):
|
||||
@ -378,20 +415,20 @@ class Locator(object):
|
||||
if prereleases or not vcls(k).is_prerelease:
|
||||
slist.append(k)
|
||||
except Exception: # pragma: no cover
|
||||
logger.warning('error matching %s with %r', matcher, k)
|
||||
logger.warning("error matching %s with %r", matcher, k)
|
||||
pass # slist.append(k)
|
||||
if len(slist) > 1:
|
||||
slist = sorted(slist, key=scheme.key)
|
||||
if slist:
|
||||
logger.debug('sorted list: %s', slist)
|
||||
logger.debug("sorted list: %s", slist)
|
||||
version = slist[-1]
|
||||
result = versions[version]
|
||||
if result:
|
||||
if r.extras:
|
||||
result.extras = r.extras
|
||||
result.download_urls = versions.get('urls', {}).get(version, set())
|
||||
result.download_urls = versions.get("urls", {}).get(version, set())
|
||||
d = {}
|
||||
sd = versions.get('digests', {})
|
||||
sd = versions.get("digests", {})
|
||||
for url in result.download_urls:
|
||||
if url in sd: # pragma: no cover
|
||||
d[url] = sd[url]
|
||||
@ -424,29 +461,29 @@ class PyPIRPCLocator(Locator):
|
||||
return set(self.client.list_packages())
|
||||
|
||||
def _get_project(self, name):
|
||||
result = {'urls': {}, 'digests': {}}
|
||||
result = {"urls": {}, "digests": {}}
|
||||
versions = self.client.package_releases(name, True)
|
||||
for v in versions:
|
||||
urls = self.client.release_urls(name, v)
|
||||
data = self.client.release_data(name, v)
|
||||
metadata = Metadata(scheme=self.scheme)
|
||||
metadata.name = data['name']
|
||||
metadata.version = data['version']
|
||||
metadata.license = data.get('license')
|
||||
metadata.keywords = data.get('keywords', [])
|
||||
metadata.summary = data.get('summary')
|
||||
metadata.name = data["name"]
|
||||
metadata.version = data["version"]
|
||||
metadata.license = data.get("license")
|
||||
metadata.keywords = data.get("keywords", [])
|
||||
metadata.summary = data.get("summary")
|
||||
dist = Distribution(metadata)
|
||||
if urls:
|
||||
info = urls[0]
|
||||
metadata.source_url = info['url']
|
||||
metadata.source_url = info["url"]
|
||||
dist.digest = self._get_digest(info)
|
||||
dist.locator = self
|
||||
result[v] = dist
|
||||
for info in urls:
|
||||
url = info['url']
|
||||
url = info["url"]
|
||||
digest = self._get_digest(info)
|
||||
result['urls'].setdefault(v, set()).add(url)
|
||||
result['digests'][url] = digest
|
||||
result["urls"].setdefault(v, set()).add(url)
|
||||
result["digests"][url] = digest
|
||||
return result
|
||||
|
||||
|
||||
@ -464,34 +501,34 @@ class PyPIJSONLocator(Locator):
|
||||
"""
|
||||
Return all the distribution names known to this locator.
|
||||
"""
|
||||
raise NotImplementedError('Not available from this locator')
|
||||
raise NotImplementedError("Not available from this locator")
|
||||
|
||||
def _get_project(self, name):
|
||||
result = {'urls': {}, 'digests': {}}
|
||||
url = urljoin(self.base_url, '%s/json' % quote(name))
|
||||
result = {"urls": {}, "digests": {}}
|
||||
url = urljoin(self.base_url, "%s/json" % quote(name))
|
||||
try:
|
||||
resp = self.opener.open(url)
|
||||
data = resp.read().decode() # for now
|
||||
d = json.loads(data)
|
||||
md = Metadata(scheme=self.scheme)
|
||||
data = d['info']
|
||||
md.name = data['name']
|
||||
md.version = data['version']
|
||||
md.license = data.get('license')
|
||||
md.keywords = data.get('keywords', [])
|
||||
md.summary = data.get('summary')
|
||||
data = d["info"]
|
||||
md.name = data["name"]
|
||||
md.version = data["version"]
|
||||
md.license = data.get("license")
|
||||
md.keywords = data.get("keywords", [])
|
||||
md.summary = data.get("summary")
|
||||
dist = Distribution(md)
|
||||
dist.locator = self
|
||||
# urls = d['urls']
|
||||
result[md.version] = dist
|
||||
for info in d['urls']:
|
||||
url = info['url']
|
||||
for info in d["urls"]:
|
||||
url = info["url"]
|
||||
dist.download_urls.add(url)
|
||||
dist.digests[url] = self._get_digest(info)
|
||||
result['urls'].setdefault(md.version, set()).add(url)
|
||||
result['digests'][url] = self._get_digest(info)
|
||||
result["urls"].setdefault(md.version, set()).add(url)
|
||||
result["digests"][url] = self._get_digest(info)
|
||||
# Now get other releases
|
||||
for version, infos in d['releases'].items():
|
||||
for version, infos in d["releases"].items():
|
||||
if version == md.version:
|
||||
continue # already done
|
||||
omd = Metadata(scheme=self.scheme)
|
||||
@ -501,24 +538,23 @@ class PyPIJSONLocator(Locator):
|
||||
odist.locator = self
|
||||
result[version] = odist
|
||||
for info in infos:
|
||||
url = info['url']
|
||||
url = info["url"]
|
||||
odist.download_urls.add(url)
|
||||
odist.digests[url] = self._get_digest(info)
|
||||
result['urls'].setdefault(version, set()).add(url)
|
||||
result['digests'][url] = self._get_digest(info)
|
||||
result["urls"].setdefault(version, set()).add(url)
|
||||
result["digests"][url] = self._get_digest(info)
|
||||
|
||||
|
||||
# for info in urls:
|
||||
# md.source_url = info['url']
|
||||
# dist.digest = self._get_digest(info)
|
||||
# dist.locator = self
|
||||
# for info in urls:
|
||||
# url = info['url']
|
||||
# result['urls'].setdefault(md.version, set()).add(url)
|
||||
# result['digests'][url] = self._get_digest(info)
|
||||
# for info in urls:
|
||||
# md.source_url = info['url']
|
||||
# dist.digest = self._get_digest(info)
|
||||
# dist.locator = self
|
||||
# for info in urls:
|
||||
# url = info['url']
|
||||
# result['urls'].setdefault(md.version, set()).add(url)
|
||||
# result['digests'][url] = self._get_digest(info)
|
||||
except Exception as e:
|
||||
self.errors.put(text_type(e))
|
||||
logger.exception('JSON fetch failed: %s', e)
|
||||
logger.exception("JSON fetch failed: %s", e)
|
||||
return result
|
||||
|
||||
|
||||
@ -526,6 +562,7 @@ class Page(object):
|
||||
"""
|
||||
This class represents a scraped HTML page.
|
||||
"""
|
||||
|
||||
# The following slightly hairy-looking regex just looks for the contents of
|
||||
# an anchor link, which has an attribute "href" either immediately preceded
|
||||
# or immediately followed by a "rel" attribute. The attribute values can be
|
||||
@ -536,7 +573,9 @@ class Page(object):
|
||||
(rel\\s*=\\s*(?:"(?P<rel1>[^"]*)"|'(?P<rel2>[^']*)'|(?P<rel3>[^>\\s\n]*))\\s+)?
|
||||
href\\s*=\\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\\s\n]*))
|
||||
(\\s+rel\\s*=\\s*(?:"(?P<rel4>[^"]*)"|'(?P<rel5>[^']*)'|(?P<rel6>[^>\\s\n]*)))?
|
||||
""", re.I | re.S | re.X)
|
||||
""",
|
||||
re.I | re.S | re.X,
|
||||
)
|
||||
_base = re.compile(r"""<base\s+href\s*=\s*['"]?([^'">]+)""", re.I | re.S)
|
||||
|
||||
def __init__(self, data, url):
|
||||
@ -550,7 +589,7 @@ href\\s*=\\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\\s\n]*))
|
||||
if m:
|
||||
self.base_url = m.group(1)
|
||||
|
||||
_clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I)
|
||||
_clean_re = re.compile(r"[^a-z0-9$&+,/:;=?@.#%_\\|-]", re.I)
|
||||
|
||||
@cached_property
|
||||
def links(self):
|
||||
@ -567,12 +606,19 @@ href\\s*=\\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\\s\n]*))
|
||||
|
||||
result = set()
|
||||
for match in self._href.finditer(self.data):
|
||||
d = match.groupdict('')
|
||||
rel = (d['rel1'] or d['rel2'] or d['rel3'] or d['rel4'] or d['rel5'] or d['rel6'])
|
||||
url = d['url1'] or d['url2'] or d['url3']
|
||||
d = match.groupdict("")
|
||||
rel = (
|
||||
d["rel1"]
|
||||
or d["rel2"]
|
||||
or d["rel3"]
|
||||
or d["rel4"]
|
||||
or d["rel5"]
|
||||
or d["rel6"]
|
||||
)
|
||||
url = d["url1"] or d["url2"] or d["url3"]
|
||||
url = urljoin(self.base_url, url)
|
||||
url = unescape(url)
|
||||
url = self._clean_re.sub(lambda m: '%%%2x' % ord(m.group(0)), url)
|
||||
url = self._clean_re.sub(lambda m: "%%%2x" % ord(m.group(0)), url)
|
||||
result.add((url, rel))
|
||||
# We sort the result, hoping to bring the most recent versions
|
||||
# to the front
|
||||
@ -589,9 +635,9 @@ class SimpleScrapingLocator(Locator):
|
||||
|
||||
# These are used to deal with various Content-Encoding schemes.
|
||||
decoders = {
|
||||
'deflate': zlib.decompress,
|
||||
'gzip': lambda b: gzip.GzipFile(fileobj=BytesIO(b)).read(),
|
||||
'none': lambda b: b,
|
||||
"deflate": zlib.decompress,
|
||||
"gzip": lambda b: gzip.GzipFile(fileobj=BytesIO(b)).read(),
|
||||
"none": lambda b: b,
|
||||
}
|
||||
|
||||
def __init__(self, url, timeout=None, num_workers=10, **kwargs):
|
||||
@ -648,16 +694,16 @@ class SimpleScrapingLocator(Locator):
|
||||
self._threads = []
|
||||
|
||||
def _get_project(self, name):
|
||||
result = {'urls': {}, 'digests': {}}
|
||||
result = {"urls": {}, "digests": {}}
|
||||
with self._gplock:
|
||||
self.result = result
|
||||
self.project_name = name
|
||||
url = urljoin(self.base_url, '%s/' % quote(name))
|
||||
url = urljoin(self.base_url, "%s/" % quote(name))
|
||||
self._seen.clear()
|
||||
self._page_cache.clear()
|
||||
self._prepare_threads()
|
||||
try:
|
||||
logger.debug('Queueing %s', url)
|
||||
logger.debug("Queueing %s", url)
|
||||
self._to_fetch.put(url)
|
||||
self._to_fetch.join()
|
||||
finally:
|
||||
@ -665,8 +711,9 @@ class SimpleScrapingLocator(Locator):
|
||||
del self.result
|
||||
return result
|
||||
|
||||
platform_dependent = re.compile(r'\b(linux_(i\d86|x86_64|arm\w+)|'
|
||||
r'win(32|_amd64)|macosx_?\d+)\b', re.I)
|
||||
platform_dependent = re.compile(
|
||||
r"\b(linux_(i\d86|x86_64|arm\w+)|" r"win(32|_amd64)|macosx_?\d+)\b", re.I
|
||||
)
|
||||
|
||||
def _is_platform_dependent(self, url):
|
||||
"""
|
||||
@ -688,7 +735,7 @@ class SimpleScrapingLocator(Locator):
|
||||
info = None
|
||||
else:
|
||||
info = self.convert_url_to_download_info(url, self.project_name)
|
||||
logger.debug('process_download: %s -> %s', url, info)
|
||||
logger.debug("process_download: %s -> %s", url, info)
|
||||
if info:
|
||||
with self._lock: # needed because self.result is shared
|
||||
self._update_version_data(self.result, info)
|
||||
@ -700,25 +747,27 @@ class SimpleScrapingLocator(Locator):
|
||||
particular "rel" attribute should be queued for scraping.
|
||||
"""
|
||||
scheme, netloc, path, _, _, _ = urlparse(link)
|
||||
if path.endswith(self.source_extensions + self.binary_extensions + self.excluded_extensions):
|
||||
if path.endswith(
|
||||
self.source_extensions + self.binary_extensions + self.excluded_extensions
|
||||
):
|
||||
result = False
|
||||
elif self.skip_externals and not link.startswith(self.base_url):
|
||||
result = False
|
||||
elif not referrer.startswith(self.base_url):
|
||||
result = False
|
||||
elif rel not in ('homepage', 'download'):
|
||||
elif rel not in ("homepage", "download"):
|
||||
result = False
|
||||
elif scheme not in ('http', 'https', 'ftp'):
|
||||
elif scheme not in ("http", "https", "ftp"):
|
||||
result = False
|
||||
elif self._is_platform_dependent(link):
|
||||
result = False
|
||||
else:
|
||||
host = netloc.split(':', 1)[0]
|
||||
if host.lower() == 'localhost':
|
||||
host = netloc.split(":", 1)[0]
|
||||
if host.lower() == "localhost":
|
||||
result = False
|
||||
else:
|
||||
result = True
|
||||
logger.debug('should_queue: %s (%s) from %s -> %s', link, rel, referrer, result)
|
||||
logger.debug("should_queue: %s (%s) from %s -> %s", link, rel, referrer, result)
|
||||
return result
|
||||
|
||||
def _fetch(self):
|
||||
@ -739,8 +788,10 @@ class SimpleScrapingLocator(Locator):
|
||||
if link not in self._seen:
|
||||
try:
|
||||
self._seen.add(link)
|
||||
if (not self._process_download(link) and self._should_queue(link, url, rel)):
|
||||
logger.debug('Queueing %s from %s', link, url)
|
||||
if not self._process_download(
|
||||
link
|
||||
) and self._should_queue(link, url, rel):
|
||||
logger.debug("Queueing %s from %s", link, url)
|
||||
self._to_fetch.put(link)
|
||||
except MetadataInvalidError: # e.g. invalid versions
|
||||
pass
|
||||
@ -763,56 +814,56 @@ class SimpleScrapingLocator(Locator):
|
||||
"""
|
||||
# http://peak.telecommunity.com/DevCenter/EasyInstall#package-index-api
|
||||
scheme, netloc, path, _, _, _ = urlparse(url)
|
||||
if scheme == 'file' and os.path.isdir(url2pathname(path)):
|
||||
url = urljoin(ensure_slash(url), 'index.html')
|
||||
if scheme == "file" and os.path.isdir(url2pathname(path)):
|
||||
url = urljoin(ensure_slash(url), "index.html")
|
||||
|
||||
if url in self._page_cache:
|
||||
result = self._page_cache[url]
|
||||
logger.debug('Returning %s from cache: %s', url, result)
|
||||
logger.debug("Returning %s from cache: %s", url, result)
|
||||
else:
|
||||
host = netloc.split(':', 1)[0]
|
||||
host = netloc.split(":", 1)[0]
|
||||
result = None
|
||||
if host in self._bad_hosts:
|
||||
logger.debug('Skipping %s due to bad host %s', url, host)
|
||||
logger.debug("Skipping %s due to bad host %s", url, host)
|
||||
else:
|
||||
req = Request(url, headers={'Accept-encoding': 'identity'})
|
||||
req = Request(url, headers={"Accept-encoding": "identity"})
|
||||
try:
|
||||
logger.debug('Fetching %s', url)
|
||||
logger.debug("Fetching %s", url)
|
||||
resp = self.opener.open(req, timeout=self.timeout)
|
||||
logger.debug('Fetched %s', url)
|
||||
logger.debug("Fetched %s", url)
|
||||
headers = resp.info()
|
||||
content_type = headers.get('Content-Type', '')
|
||||
content_type = headers.get("Content-Type", "")
|
||||
if HTML_CONTENT_TYPE.match(content_type):
|
||||
final_url = resp.geturl()
|
||||
data = resp.read()
|
||||
encoding = headers.get('Content-Encoding')
|
||||
encoding = headers.get("Content-Encoding")
|
||||
if encoding:
|
||||
decoder = self.decoders[encoding] # fail if not found
|
||||
data = decoder(data)
|
||||
encoding = 'utf-8'
|
||||
encoding = "utf-8"
|
||||
m = CHARSET.search(content_type)
|
||||
if m:
|
||||
encoding = m.group(1)
|
||||
try:
|
||||
data = data.decode(encoding)
|
||||
except UnicodeError: # pragma: no cover
|
||||
data = data.decode('latin-1') # fallback
|
||||
data = data.decode("latin-1") # fallback
|
||||
result = Page(data, final_url)
|
||||
self._page_cache[final_url] = result
|
||||
except HTTPError as e:
|
||||
if e.code != 404:
|
||||
logger.exception('Fetch failed: %s: %s', url, e)
|
||||
logger.exception("Fetch failed: %s: %s", url, e)
|
||||
except URLError as e: # pragma: no cover
|
||||
logger.exception('Fetch failed: %s: %s', url, e)
|
||||
logger.exception("Fetch failed: %s: %s", url, e)
|
||||
with self._lock:
|
||||
self._bad_hosts.add(host)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception('Fetch failed: %s: %s', url, e)
|
||||
logger.exception("Fetch failed: %s: %s", url, e)
|
||||
finally:
|
||||
self._page_cache[url] = result # even if None (failure)
|
||||
return result
|
||||
|
||||
_distname_re = re.compile('<a href=[^>]*>([^<]+)<')
|
||||
_distname_re = re.compile("<a href=[^>]*>([^<]+)<")
|
||||
|
||||
def get_distribution_names(self):
|
||||
"""
|
||||
@ -821,7 +872,7 @@ class SimpleScrapingLocator(Locator):
|
||||
result = set()
|
||||
page = self.get_page(self.base_url)
|
||||
if not page:
|
||||
raise DistlibException('Unable to get %s' % self.base_url)
|
||||
raise DistlibException("Unable to get %s" % self.base_url)
|
||||
for match in self._distname_re.finditer(page.data):
|
||||
result.add(match.group(1))
|
||||
return result
|
||||
@ -842,11 +893,11 @@ class DirectoryLocator(Locator):
|
||||
recursed into. If False, only the top-level directory
|
||||
is searched,
|
||||
"""
|
||||
self.recursive = kwargs.pop('recursive', True)
|
||||
self.recursive = kwargs.pop("recursive", True)
|
||||
super(DirectoryLocator, self).__init__(**kwargs)
|
||||
path = os.path.abspath(path)
|
||||
if not os.path.isdir(path): # pragma: no cover
|
||||
raise DistlibException('Not a directory: %r' % path)
|
||||
raise DistlibException("Not a directory: %r" % path)
|
||||
self.base_dir = path
|
||||
|
||||
def should_include(self, filename, parent):
|
||||
@ -858,12 +909,14 @@ class DirectoryLocator(Locator):
|
||||
return filename.endswith(self.downloadable_extensions)
|
||||
|
||||
def _get_project(self, name):
|
||||
result = {'urls': {}, 'digests': {}}
|
||||
result = {"urls": {}, "digests": {}}
|
||||
for root, dirs, files in os.walk(self.base_dir):
|
||||
for fn in files:
|
||||
if self.should_include(fn, root):
|
||||
fn = os.path.join(root, fn)
|
||||
url = urlunparse(('file', '', pathname2url(os.path.abspath(fn)), '', '', ''))
|
||||
url = urlunparse(
|
||||
("file", "", pathname2url(os.path.abspath(fn)), "", "", "")
|
||||
)
|
||||
info = self.convert_url_to_download_info(url, name)
|
||||
if info:
|
||||
self._update_version_data(result, info)
|
||||
@ -880,10 +933,12 @@ class DirectoryLocator(Locator):
|
||||
for fn in files:
|
||||
if self.should_include(fn, root):
|
||||
fn = os.path.join(root, fn)
|
||||
url = urlunparse(('file', '', pathname2url(os.path.abspath(fn)), '', '', ''))
|
||||
url = urlunparse(
|
||||
("file", "", pathname2url(os.path.abspath(fn)), "", "", "")
|
||||
)
|
||||
info = self.convert_url_to_download_info(url, None)
|
||||
if info:
|
||||
result.add(info['name'])
|
||||
result.add(info["name"])
|
||||
if not self.recursive:
|
||||
break
|
||||
return result
|
||||
@ -901,31 +956,33 @@ class JSONLocator(Locator):
|
||||
"""
|
||||
Return all the distribution names known to this locator.
|
||||
"""
|
||||
raise NotImplementedError('Not available from this locator')
|
||||
raise NotImplementedError("Not available from this locator")
|
||||
|
||||
def _get_project(self, name):
|
||||
result = {'urls': {}, 'digests': {}}
|
||||
result = {"urls": {}, "digests": {}}
|
||||
data = get_project_data(name)
|
||||
if data:
|
||||
for info in data.get('files', []):
|
||||
if info['ptype'] != 'sdist' or info['pyversion'] != 'source':
|
||||
for info in data.get("files", []):
|
||||
if info["ptype"] != "sdist" or info["pyversion"] != "source":
|
||||
continue
|
||||
# We don't store summary in project metadata as it makes
|
||||
# the data bigger for no benefit during dependency
|
||||
# resolution
|
||||
dist = make_dist(data['name'],
|
||||
info['version'],
|
||||
summary=data.get('summary', 'Placeholder for summary'),
|
||||
scheme=self.scheme)
|
||||
dist = make_dist(
|
||||
data["name"],
|
||||
info["version"],
|
||||
summary=data.get("summary", "Placeholder for summary"),
|
||||
scheme=self.scheme,
|
||||
)
|
||||
md = dist.metadata
|
||||
md.source_url = info['url']
|
||||
md.source_url = info["url"]
|
||||
# TODO SHA256 digest
|
||||
if 'digest' in info and info['digest']:
|
||||
dist.digest = ('md5', info['digest'])
|
||||
md.dependencies = info.get('requirements', {})
|
||||
dist.exports = info.get('exports', {})
|
||||
if "digest" in info and info["digest"]:
|
||||
dist.digest = ("md5", info["digest"])
|
||||
md.dependencies = info.get("requirements", {})
|
||||
dist.exports = info.get("exports", {})
|
||||
result[dist.version] = dist
|
||||
result['urls'].setdefault(dist.version, set()).add(info['url'])
|
||||
result["urls"].setdefault(dist.version, set()).add(info["url"])
|
||||
return result
|
||||
|
||||
|
||||
@ -948,16 +1005,12 @@ class DistPathLocator(Locator):
|
||||
def _get_project(self, name):
|
||||
dist = self.distpath.get_distribution(name)
|
||||
if dist is None:
|
||||
result = {'urls': {}, 'digests': {}}
|
||||
result = {"urls": {}, "digests": {}}
|
||||
else:
|
||||
result = {
|
||||
dist.version: dist,
|
||||
'urls': {
|
||||
dist.version: set([dist.source_url])
|
||||
},
|
||||
'digests': {
|
||||
dist.version: set([None])
|
||||
}
|
||||
"urls": {dist.version: set([dist.source_url])},
|
||||
"digests": {dist.version: set([None])},
|
||||
}
|
||||
return result
|
||||
|
||||
@ -979,7 +1032,7 @@ class AggregatingLocator(Locator):
|
||||
the results from all locators are merged (this can be
|
||||
slow).
|
||||
"""
|
||||
self.merge = kwargs.pop('merge', False)
|
||||
self.merge = kwargs.pop("merge", False)
|
||||
self.locators = locators
|
||||
super(AggregatingLocator, self).__init__(**kwargs)
|
||||
|
||||
@ -1001,18 +1054,18 @@ class AggregatingLocator(Locator):
|
||||
d = locator.get_project(name)
|
||||
if d:
|
||||
if self.merge:
|
||||
files = result.get('urls', {})
|
||||
digests = result.get('digests', {})
|
||||
files = result.get("urls", {})
|
||||
digests = result.get("digests", {})
|
||||
# next line could overwrite result['urls'], result['digests']
|
||||
result.update(d)
|
||||
df = result.get('urls')
|
||||
df = result.get("urls")
|
||||
if files and df:
|
||||
for k, v in files.items():
|
||||
if k in df:
|
||||
df[k] |= v
|
||||
else:
|
||||
df[k] = v
|
||||
dd = result.get('digests')
|
||||
dd = result.get("digests")
|
||||
if digests and dd:
|
||||
dd.update(digests)
|
||||
else:
|
||||
@ -1056,8 +1109,9 @@ class AggregatingLocator(Locator):
|
||||
# versions which don't conform to PEP 440.
|
||||
default_locator = AggregatingLocator(
|
||||
# JSONLocator(), # don't use as PEP 426 is withdrawn
|
||||
SimpleScrapingLocator('https://pypi.org/simple/', timeout=3.0),
|
||||
scheme='legacy')
|
||||
SimpleScrapingLocator("https://pypi.org/simple/", timeout=3.0),
|
||||
scheme="legacy",
|
||||
)
|
||||
|
||||
locate = default_locator.locate
|
||||
|
||||
@ -1081,13 +1135,13 @@ class DependencyFinder(object):
|
||||
about who provides what.
|
||||
:param dist: The distribution to add.
|
||||
"""
|
||||
logger.debug('adding distribution %s', dist)
|
||||
logger.debug("adding distribution %s", dist)
|
||||
name = dist.key
|
||||
self.dists_by_name[name] = dist
|
||||
self.dists[(name, dist.version)] = dist
|
||||
for p in dist.provides:
|
||||
name, version = parse_name_and_version(p)
|
||||
logger.debug('Add to provided: %s, %s, %s', name, version, dist)
|
||||
logger.debug("Add to provided: %s, %s, %s", name, version, dist)
|
||||
self.provided.setdefault(name, set()).add((version, dist))
|
||||
|
||||
def remove_distribution(self, dist):
|
||||
@ -1096,13 +1150,13 @@ class DependencyFinder(object):
|
||||
information about who provides what.
|
||||
:param dist: The distribution to remove.
|
||||
"""
|
||||
logger.debug('removing distribution %s', dist)
|
||||
logger.debug("removing distribution %s", dist)
|
||||
name = dist.key
|
||||
del self.dists_by_name[name]
|
||||
del self.dists[(name, dist.version)]
|
||||
for p in dist.provides:
|
||||
name, version = parse_name_and_version(p)
|
||||
logger.debug('Remove from provided: %s, %s, %s', name, version, dist)
|
||||
logger.debug("Remove from provided: %s, %s, %s", name, version, dist)
|
||||
s = self.provided[name]
|
||||
s.remove((version, dist))
|
||||
if not s:
|
||||
@ -1175,7 +1229,7 @@ class DependencyFinder(object):
|
||||
unmatched.add(s)
|
||||
if unmatched:
|
||||
# can't replace other with provider
|
||||
problems.add(('cantreplace', provider, other, frozenset(unmatched)))
|
||||
problems.add(("cantreplace", provider, other, frozenset(unmatched)))
|
||||
result = False
|
||||
else:
|
||||
# can replace other with provider
|
||||
@ -1219,19 +1273,19 @@ class DependencyFinder(object):
|
||||
self.reqts = {}
|
||||
|
||||
meta_extras = set(meta_extras or [])
|
||||
if ':*:' in meta_extras:
|
||||
meta_extras.remove(':*:')
|
||||
if ":*:" in meta_extras:
|
||||
meta_extras.remove(":*:")
|
||||
# :meta: and :run: are implicitly included
|
||||
meta_extras |= set([':test:', ':build:', ':dev:'])
|
||||
meta_extras |= set([":test:", ":build:", ":dev:"])
|
||||
|
||||
if isinstance(requirement, Distribution):
|
||||
dist = odist = requirement
|
||||
logger.debug('passed %s as requirement', odist)
|
||||
logger.debug("passed %s as requirement", odist)
|
||||
else:
|
||||
dist = odist = self.locator.locate(requirement, prereleases=prereleases)
|
||||
if dist is None:
|
||||
raise DistlibException('Unable to locate %r' % requirement)
|
||||
logger.debug('located %s', odist)
|
||||
raise DistlibException("Unable to locate %r" % requirement)
|
||||
logger.debug("located %s", odist)
|
||||
dist.requested = True
|
||||
problems = set()
|
||||
todo = set([dist])
|
||||
@ -1251,23 +1305,23 @@ class DependencyFinder(object):
|
||||
sreqts = dist.build_requires
|
||||
ereqts = set()
|
||||
if meta_extras and dist in install_dists:
|
||||
for key in ('test', 'build', 'dev'):
|
||||
e = ':%s:' % key
|
||||
for key in ("test", "build", "dev"):
|
||||
e = ":%s:" % key
|
||||
if e in meta_extras:
|
||||
ereqts |= getattr(dist, '%s_requires' % key)
|
||||
ereqts |= getattr(dist, "%s_requires" % key)
|
||||
all_reqts = ireqts | sreqts | ereqts
|
||||
for r in all_reqts:
|
||||
providers = self.find_providers(r)
|
||||
if not providers:
|
||||
logger.debug('No providers found for %r', r)
|
||||
logger.debug("No providers found for %r", r)
|
||||
provider = self.locator.locate(r, prereleases=prereleases)
|
||||
# If no provider is found and we didn't consider
|
||||
# prereleases, consider them now.
|
||||
if provider is None and not prereleases:
|
||||
provider = self.locator.locate(r, prereleases=True)
|
||||
if provider is None:
|
||||
logger.debug('Cannot satisfy %r', r)
|
||||
problems.add(('unsatisfied', r))
|
||||
logger.debug("Cannot satisfy %r", r)
|
||||
problems.add(("unsatisfied", r))
|
||||
else:
|
||||
n, v = provider.key, provider.version
|
||||
if (n, v) not in self.dists:
|
||||
@ -1275,7 +1329,9 @@ class DependencyFinder(object):
|
||||
providers.add(provider)
|
||||
if r in ireqts and dist in install_dists:
|
||||
install_dists.add(provider)
|
||||
logger.debug('Adding %s to install_dists', provider.name_and_version)
|
||||
logger.debug(
|
||||
"Adding %s to install_dists", provider.name_and_version
|
||||
)
|
||||
for p in providers:
|
||||
name = p.key
|
||||
if name not in self.dists_by_name:
|
||||
@ -1290,6 +1346,8 @@ class DependencyFinder(object):
|
||||
for dist in dists:
|
||||
dist.build_time_dependency = dist not in install_dists
|
||||
if dist.build_time_dependency:
|
||||
logger.debug('%s is a build-time dependency only.', dist.name_and_version)
|
||||
logger.debug('find done for %s', odist)
|
||||
logger.debug(
|
||||
"%s is a build-time dependency only.", dist.name_and_version
|
||||
)
|
||||
logger.debug("find done for %s", odist)
|
||||
return dists, problems
|
||||
|
||||
@ -8,6 +8,7 @@ Class representing the list of files in a distribution.
|
||||
|
||||
Equivalent to distutils.filelist, but fixes some problems.
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
@ -18,14 +19,13 @@ from . import DistlibException
|
||||
from .compat import fsdecode
|
||||
from .util import convert_path
|
||||
|
||||
|
||||
__all__ = ['Manifest']
|
||||
__all__ = ["Manifest"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# a \ followed by some spaces + EOL
|
||||
_COLLAPSE_PATTERN = re.compile('\\\\w*\n', re.M)
|
||||
_COMMENTED_LINE = re.compile('#.*?(?=\n)|\n(?=$)', re.M | re.S)
|
||||
_COLLAPSE_PATTERN = re.compile("\\\\w*\n", re.M)
|
||||
_COMMENTED_LINE = re.compile("#.*?(?=\n)|\n(?=$)", re.M | re.S)
|
||||
|
||||
#
|
||||
# Due to the different results returned by fnmatch.translate, we need
|
||||
@ -109,10 +109,10 @@ class Manifest(object):
|
||||
|
||||
def add_dir(dirs, d):
|
||||
dirs.add(d)
|
||||
logger.debug('add_dir added %s', d)
|
||||
logger.debug("add_dir added %s", d)
|
||||
if d != self.base:
|
||||
parent, _ = os.path.split(d)
|
||||
assert parent not in ('', '/')
|
||||
assert parent not in ("", "/")
|
||||
add_dir(dirs, parent)
|
||||
|
||||
result = set(self.files) # make a copy!
|
||||
@ -121,8 +121,10 @@ class Manifest(object):
|
||||
for f in result:
|
||||
add_dir(dirs, os.path.dirname(f))
|
||||
result |= dirs
|
||||
return [os.path.join(*path_tuple) for path_tuple in
|
||||
sorted(os.path.split(path) for path in result)]
|
||||
return [
|
||||
os.path.join(*path_tuple)
|
||||
for path_tuple in sorted(os.path.split(path) for path in result)
|
||||
]
|
||||
|
||||
def clear(self):
|
||||
"""Clear all collected files."""
|
||||
@ -149,49 +151,54 @@ class Manifest(object):
|
||||
# OK, now we know that the action is valid and we have the
|
||||
# right number of words on the line for that action -- so we
|
||||
# can proceed with minimal error-checking.
|
||||
if action == 'include':
|
||||
if action == "include":
|
||||
for pattern in patterns:
|
||||
if not self._include_pattern(pattern, anchor=True):
|
||||
logger.warning('no files found matching %r', pattern)
|
||||
logger.warning("no files found matching %r", pattern)
|
||||
|
||||
elif action == 'exclude':
|
||||
elif action == "exclude":
|
||||
for pattern in patterns:
|
||||
self._exclude_pattern(pattern, anchor=True)
|
||||
|
||||
elif action == 'global-include':
|
||||
elif action == "global-include":
|
||||
for pattern in patterns:
|
||||
if not self._include_pattern(pattern, anchor=False):
|
||||
logger.warning('no files found matching %r '
|
||||
'anywhere in distribution', pattern)
|
||||
logger.warning(
|
||||
"no files found matching %r " "anywhere in distribution",
|
||||
pattern,
|
||||
)
|
||||
|
||||
elif action == 'global-exclude':
|
||||
elif action == "global-exclude":
|
||||
for pattern in patterns:
|
||||
self._exclude_pattern(pattern, anchor=False)
|
||||
|
||||
elif action == 'recursive-include':
|
||||
elif action == "recursive-include":
|
||||
for pattern in patterns:
|
||||
if not self._include_pattern(pattern, prefix=thedir):
|
||||
logger.warning('no files found matching %r '
|
||||
'under directory %r', pattern, thedir)
|
||||
logger.warning(
|
||||
"no files found matching %r " "under directory %r",
|
||||
pattern,
|
||||
thedir,
|
||||
)
|
||||
|
||||
elif action == 'recursive-exclude':
|
||||
elif action == "recursive-exclude":
|
||||
for pattern in patterns:
|
||||
self._exclude_pattern(pattern, prefix=thedir)
|
||||
|
||||
elif action == 'graft':
|
||||
elif action == "graft":
|
||||
if not self._include_pattern(None, prefix=dirpattern):
|
||||
logger.warning('no directories found matching %r',
|
||||
dirpattern)
|
||||
logger.warning("no directories found matching %r", dirpattern)
|
||||
|
||||
elif action == 'prune':
|
||||
elif action == "prune":
|
||||
if not self._exclude_pattern(None, prefix=dirpattern):
|
||||
logger.warning('no previously-included directories found '
|
||||
'matching %r', dirpattern)
|
||||
logger.warning(
|
||||
"no previously-included directories found " "matching %r",
|
||||
dirpattern,
|
||||
)
|
||||
else: # pragma: no cover
|
||||
# This should never happen, as it should be caught in
|
||||
# _parse_template_line
|
||||
raise DistlibException(
|
||||
'invalid action %r' % action)
|
||||
raise DistlibException("invalid action %r" % action)
|
||||
|
||||
#
|
||||
# Private API
|
||||
@ -204,48 +211,49 @@ class Manifest(object):
|
||||
:return: A tuple of action, patterns, thedir, dir_patterns
|
||||
"""
|
||||
words = directive.split()
|
||||
if len(words) == 1 and words[0] not in ('include', 'exclude',
|
||||
'global-include',
|
||||
'global-exclude',
|
||||
'recursive-include',
|
||||
'recursive-exclude',
|
||||
'graft', 'prune'):
|
||||
if len(words) == 1 and words[0] not in (
|
||||
"include",
|
||||
"exclude",
|
||||
"global-include",
|
||||
"global-exclude",
|
||||
"recursive-include",
|
||||
"recursive-exclude",
|
||||
"graft",
|
||||
"prune",
|
||||
):
|
||||
# no action given, let's use the default 'include'
|
||||
words.insert(0, 'include')
|
||||
words.insert(0, "include")
|
||||
|
||||
action = words[0]
|
||||
patterns = thedir = dir_pattern = None
|
||||
|
||||
if action in ('include', 'exclude',
|
||||
'global-include', 'global-exclude'):
|
||||
if action in ("include", "exclude", "global-include", "global-exclude"):
|
||||
if len(words) < 2:
|
||||
raise DistlibException(
|
||||
'%r expects <pattern1> <pattern2> ...' % action)
|
||||
raise DistlibException("%r expects <pattern1> <pattern2> ..." % action)
|
||||
|
||||
patterns = [convert_path(word) for word in words[1:]]
|
||||
|
||||
elif action in ('recursive-include', 'recursive-exclude'):
|
||||
elif action in ("recursive-include", "recursive-exclude"):
|
||||
if len(words) < 3:
|
||||
raise DistlibException(
|
||||
'%r expects <dir> <pattern1> <pattern2> ...' % action)
|
||||
"%r expects <dir> <pattern1> <pattern2> ..." % action
|
||||
)
|
||||
|
||||
thedir = convert_path(words[1])
|
||||
patterns = [convert_path(word) for word in words[2:]]
|
||||
|
||||
elif action in ('graft', 'prune'):
|
||||
elif action in ("graft", "prune"):
|
||||
if len(words) != 2:
|
||||
raise DistlibException(
|
||||
'%r expects a single <dir_pattern>' % action)
|
||||
raise DistlibException("%r expects a single <dir_pattern>" % action)
|
||||
|
||||
dir_pattern = convert_path(words[1])
|
||||
|
||||
else:
|
||||
raise DistlibException('unknown action %r' % action)
|
||||
raise DistlibException("unknown action %r" % action)
|
||||
|
||||
return action, patterns, thedir, dir_pattern
|
||||
|
||||
def _include_pattern(self, pattern, anchor=True, prefix=None,
|
||||
is_regex=False):
|
||||
def _include_pattern(self, pattern, anchor=True, prefix=None, is_regex=False):
|
||||
"""Select strings (presumably filenames) from 'self.files' that
|
||||
match 'pattern', a Unix-style wildcard (glob) pattern.
|
||||
|
||||
@ -285,8 +293,7 @@ class Manifest(object):
|
||||
found = True
|
||||
return found
|
||||
|
||||
def _exclude_pattern(self, pattern, anchor=True, prefix=None,
|
||||
is_regex=False):
|
||||
def _exclude_pattern(self, pattern, anchor=True, prefix=None, is_regex=False):
|
||||
"""Remove strings (presumably filenames) from 'files' that match
|
||||
'pattern'.
|
||||
|
||||
@ -305,8 +312,7 @@ class Manifest(object):
|
||||
found = True
|
||||
return found
|
||||
|
||||
def _translate_pattern(self, pattern, anchor=True, prefix=None,
|
||||
is_regex=False):
|
||||
def _translate_pattern(self, pattern, anchor=True, prefix=None, is_regex=False):
|
||||
"""Translate a shell-like wildcard pattern to a compiled regular
|
||||
expression.
|
||||
|
||||
@ -322,41 +328,46 @@ class Manifest(object):
|
||||
|
||||
if _PYTHON_VERSION > (3, 2):
|
||||
# ditch start and end characters
|
||||
start, _, end = self._glob_to_re('_').partition('_')
|
||||
start, _, end = self._glob_to_re("_").partition("_")
|
||||
|
||||
if pattern:
|
||||
pattern_re = self._glob_to_re(pattern)
|
||||
if _PYTHON_VERSION > (3, 2):
|
||||
assert pattern_re.startswith(start) and pattern_re.endswith(end)
|
||||
else:
|
||||
pattern_re = ''
|
||||
pattern_re = ""
|
||||
|
||||
base = re.escape(os.path.join(self.base, ''))
|
||||
base = re.escape(os.path.join(self.base, ""))
|
||||
if prefix is not None:
|
||||
# ditch end of pattern character
|
||||
if _PYTHON_VERSION <= (3, 2):
|
||||
empty_pattern = self._glob_to_re('')
|
||||
prefix_re = self._glob_to_re(prefix)[:-len(empty_pattern)]
|
||||
empty_pattern = self._glob_to_re("")
|
||||
prefix_re = self._glob_to_re(prefix)[: -len(empty_pattern)]
|
||||
else:
|
||||
prefix_re = self._glob_to_re(prefix)
|
||||
assert prefix_re.startswith(start) and prefix_re.endswith(end)
|
||||
prefix_re = prefix_re[len(start): len(prefix_re) - len(end)]
|
||||
prefix_re = prefix_re[len(start) : len(prefix_re) - len(end)]
|
||||
sep = os.sep
|
||||
if os.sep == '\\':
|
||||
sep = r'\\'
|
||||
if os.sep == "\\":
|
||||
sep = r"\\"
|
||||
if _PYTHON_VERSION <= (3, 2):
|
||||
pattern_re = '^' + base + sep.join((prefix_re,
|
||||
'.*' + pattern_re))
|
||||
pattern_re = "^" + base + sep.join((prefix_re, ".*" + pattern_re))
|
||||
else:
|
||||
pattern_re = pattern_re[len(start): len(pattern_re) - len(end)]
|
||||
pattern_re = r'%s%s%s%s.*%s%s' % (start, base, prefix_re, sep,
|
||||
pattern_re, end)
|
||||
pattern_re = pattern_re[len(start) : len(pattern_re) - len(end)]
|
||||
pattern_re = r"%s%s%s%s.*%s%s" % (
|
||||
start,
|
||||
base,
|
||||
prefix_re,
|
||||
sep,
|
||||
pattern_re,
|
||||
end,
|
||||
)
|
||||
else: # no prefix -- respect anchor flag
|
||||
if anchor:
|
||||
if _PYTHON_VERSION <= (3, 2):
|
||||
pattern_re = '^' + base + pattern_re
|
||||
pattern_re = "^" + base + pattern_re
|
||||
else:
|
||||
pattern_re = r'%s%s%s' % (start, base, pattern_re[len(start):])
|
||||
pattern_re = r"%s%s%s" % (start, base, pattern_re[len(start) :])
|
||||
|
||||
return re.compile(pattern_re)
|
||||
|
||||
@ -375,10 +386,10 @@ class Manifest(object):
|
||||
# any OS. So change all non-escaped dots in the RE to match any
|
||||
# character except the special characters (currently: just os.sep).
|
||||
sep = os.sep
|
||||
if os.sep == '\\':
|
||||
if os.sep == "\\":
|
||||
# we're using a regex to manipulate a regex, so we need
|
||||
# to escape the backslash twice
|
||||
sep = r'\\\\'
|
||||
escaped = r'\1[^%s]' % sep
|
||||
pattern_re = re.sub(r'((?<!\\)(\\\\)*)\.', escaped, pattern_re)
|
||||
sep = r"\\\\"
|
||||
escaped = r"\1[^%s]" % sep
|
||||
pattern_re = re.sub(r"((?<!\\)(\\\\)*)\.", escaped, pattern_re)
|
||||
return pattern_re
|
||||
|
||||
@ -21,10 +21,12 @@ from .compat import string_types
|
||||
from .util import in_venv, parse_marker
|
||||
from .version import LegacyVersion as LV
|
||||
|
||||
__all__ = ['interpret']
|
||||
__all__ = ["interpret"]
|
||||
|
||||
_VERSION_PATTERN = re.compile(r'((\d+(\.\d+)*\w*)|\'(\d+(\.\d+)*\w*)\'|\"(\d+(\.\d+)*\w*)\")')
|
||||
_VERSION_MARKERS = {'python_version', 'python_full_version'}
|
||||
_VERSION_PATTERN = re.compile(
|
||||
r"((\d+(\.\d+)*\w*)|\'(\d+(\.\d+)*\w*)\'|\"(\d+(\.\d+)*\w*)\")"
|
||||
)
|
||||
_VERSION_MARKERS = {"python_version", "python_full_version"}
|
||||
|
||||
|
||||
def _is_version_marker(s):
|
||||
@ -34,7 +36,7 @@ def _is_version_marker(s):
|
||||
def _is_literal(o):
|
||||
if not isinstance(o, string_types) or not o:
|
||||
return False
|
||||
return o[0] in '\'"'
|
||||
return o[0] in "'\""
|
||||
|
||||
|
||||
def _get_versions(s):
|
||||
@ -47,18 +49,18 @@ class Evaluator(object):
|
||||
"""
|
||||
|
||||
operations = {
|
||||
'==': lambda x, y: x == y,
|
||||
'===': lambda x, y: x == y,
|
||||
'~=': lambda x, y: x == y or x > y,
|
||||
'!=': lambda x, y: x != y,
|
||||
'<': lambda x, y: x < y,
|
||||
'<=': lambda x, y: x == y or x < y,
|
||||
'>': lambda x, y: x > y,
|
||||
'>=': lambda x, y: x == y or x > y,
|
||||
'and': lambda x, y: x and y,
|
||||
'or': lambda x, y: x or y,
|
||||
'in': lambda x, y: x in y,
|
||||
'not in': lambda x, y: x not in y,
|
||||
"==": lambda x, y: x == y,
|
||||
"===": lambda x, y: x == y,
|
||||
"~=": lambda x, y: x == y or x > y,
|
||||
"!=": lambda x, y: x != y,
|
||||
"<": lambda x, y: x < y,
|
||||
"<=": lambda x, y: x == y or x < y,
|
||||
">": lambda x, y: x > y,
|
||||
">=": lambda x, y: x == y or x > y,
|
||||
"and": lambda x, y: x and y,
|
||||
"or": lambda x, y: x or y,
|
||||
"in": lambda x, y: x in y,
|
||||
"not in": lambda x, y: x not in y,
|
||||
}
|
||||
|
||||
def evaluate(self, expr, context):
|
||||
@ -67,70 +69,78 @@ class Evaluator(object):
|
||||
function in the specified context.
|
||||
"""
|
||||
if isinstance(expr, string_types):
|
||||
if expr[0] in '\'"':
|
||||
if expr[0] in "'\"":
|
||||
result = expr[1:-1]
|
||||
else:
|
||||
if expr not in context:
|
||||
raise SyntaxError('unknown variable: %s' % expr)
|
||||
raise SyntaxError("unknown variable: %s" % expr)
|
||||
result = context[expr]
|
||||
else:
|
||||
assert isinstance(expr, dict)
|
||||
op = expr['op']
|
||||
op = expr["op"]
|
||||
if op not in self.operations:
|
||||
raise NotImplementedError('op not implemented: %s' % op)
|
||||
elhs = expr['lhs']
|
||||
erhs = expr['rhs']
|
||||
if _is_literal(expr['lhs']) and _is_literal(expr['rhs']):
|
||||
raise SyntaxError('invalid comparison: %s %s %s' % (elhs, op, erhs))
|
||||
raise NotImplementedError("op not implemented: %s" % op)
|
||||
elhs = expr["lhs"]
|
||||
erhs = expr["rhs"]
|
||||
if _is_literal(expr["lhs"]) and _is_literal(expr["rhs"]):
|
||||
raise SyntaxError("invalid comparison: %s %s %s" % (elhs, op, erhs))
|
||||
|
||||
lhs = self.evaluate(elhs, context)
|
||||
rhs = self.evaluate(erhs, context)
|
||||
if ((_is_version_marker(elhs) or _is_version_marker(erhs)) and
|
||||
op in ('<', '<=', '>', '>=', '===', '==', '!=', '~=')):
|
||||
if (_is_version_marker(elhs) or _is_version_marker(erhs)) and op in (
|
||||
"<",
|
||||
"<=",
|
||||
">",
|
||||
">=",
|
||||
"===",
|
||||
"==",
|
||||
"!=",
|
||||
"~=",
|
||||
):
|
||||
lhs = LV(lhs)
|
||||
rhs = LV(rhs)
|
||||
elif _is_version_marker(elhs) and op in ('in', 'not in'):
|
||||
elif _is_version_marker(elhs) and op in ("in", "not in"):
|
||||
lhs = LV(lhs)
|
||||
rhs = _get_versions(rhs)
|
||||
result = self.operations[op](lhs, rhs)
|
||||
return result
|
||||
|
||||
|
||||
_DIGITS = re.compile(r'\d+\.\d+')
|
||||
_DIGITS = re.compile(r"\d+\.\d+")
|
||||
|
||||
|
||||
def default_context():
|
||||
|
||||
def format_full_version(info):
|
||||
version = '%s.%s.%s' % (info.major, info.minor, info.micro)
|
||||
version = "%s.%s.%s" % (info.major, info.minor, info.micro)
|
||||
kind = info.releaselevel
|
||||
if kind != 'final':
|
||||
if kind != "final":
|
||||
version += kind[0] + str(info.serial)
|
||||
return version
|
||||
|
||||
if hasattr(sys, 'implementation'):
|
||||
if hasattr(sys, "implementation"):
|
||||
implementation_version = format_full_version(sys.implementation.version)
|
||||
implementation_name = sys.implementation.name
|
||||
else:
|
||||
implementation_version = '0'
|
||||
implementation_name = ''
|
||||
implementation_version = "0"
|
||||
implementation_name = ""
|
||||
|
||||
ppv = platform.python_version()
|
||||
m = _DIGITS.match(ppv)
|
||||
pv = m.group(0)
|
||||
result = {
|
||||
'implementation_name': implementation_name,
|
||||
'implementation_version': implementation_version,
|
||||
'os_name': os.name,
|
||||
'platform_machine': platform.machine(),
|
||||
'platform_python_implementation': platform.python_implementation(),
|
||||
'platform_release': platform.release(),
|
||||
'platform_system': platform.system(),
|
||||
'platform_version': platform.version(),
|
||||
'platform_in_venv': str(in_venv()),
|
||||
'python_full_version': ppv,
|
||||
'python_version': pv,
|
||||
'sys_platform': sys.platform,
|
||||
"implementation_name": implementation_name,
|
||||
"implementation_version": implementation_version,
|
||||
"os_name": os.name,
|
||||
"platform_machine": platform.machine(),
|
||||
"platform_python_implementation": platform.python_implementation(),
|
||||
"platform_release": platform.release(),
|
||||
"platform_system": platform.system(),
|
||||
"platform_version": platform.version(),
|
||||
"platform_in_venv": str(in_venv()),
|
||||
"python_full_version": ppv,
|
||||
"python_version": pv,
|
||||
"sys_platform": sys.platform,
|
||||
}
|
||||
return result
|
||||
|
||||
@ -140,12 +150,14 @@ del default_context
|
||||
|
||||
evaluator = Evaluator()
|
||||
|
||||
|
||||
def interpret_parsed(expr, execution_context=None):
|
||||
context = dict(DEFAULT_CONTEXT)
|
||||
if execution_context:
|
||||
context.update(execution_context)
|
||||
return evaluator.evaluate(expr, context)
|
||||
|
||||
|
||||
def interpret(marker, execution_context=None):
|
||||
"""
|
||||
Interpret a marker and return a result depending on environment.
|
||||
@ -158,7 +170,7 @@ def interpret(marker, execution_context=None):
|
||||
try:
|
||||
expr, rest = parse_marker(marker)
|
||||
except Exception as e:
|
||||
raise SyntaxError('Unable to interpret marker syntax: %s: %s' % (marker, e))
|
||||
if rest and rest[0] != '#':
|
||||
raise SyntaxError('unexpected trailing data in marker: %s: %s' % (marker, rest))
|
||||
raise SyntaxError("Unable to interpret marker syntax: %s: %s" % (marker, e))
|
||||
if rest and rest[0] != "#":
|
||||
raise SyntaxError("unexpected trailing data in marker: %s: %s" % (marker, rest))
|
||||
return interpret_parsed(expr, execution_context)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,7 @@ class ResourceCache(Cache):
|
||||
def __init__(self, base=None):
|
||||
if base is None:
|
||||
# Use native string to avoid issues on 2.x: see Python #20140.
|
||||
base = os.path.join(get_cache_base(), str('resource-cache'))
|
||||
base = os.path.join(get_cache_base(), str("resource-cache"))
|
||||
super(ResourceCache, self).__init__(base)
|
||||
|
||||
def is_stale(self, resource, path):
|
||||
@ -63,7 +63,7 @@ class ResourceCache(Cache):
|
||||
stale = self.is_stale(resource, path)
|
||||
if stale:
|
||||
# write the bytes of the resource to the cache location
|
||||
with open(result, 'wb') as f:
|
||||
with open(result, "wb") as f:
|
||||
f.write(resource.bytes)
|
||||
return result
|
||||
|
||||
@ -80,6 +80,7 @@ class Resource(ResourceBase):
|
||||
not normally instantiated by user code, but rather by a
|
||||
:class:`ResourceFinder` which manages the resource.
|
||||
"""
|
||||
|
||||
is_container = False # Backwards compatibility
|
||||
|
||||
def as_stream(self):
|
||||
@ -120,15 +121,15 @@ class ResourceFinder(object):
|
||||
Resource finder for file system resources.
|
||||
"""
|
||||
|
||||
if sys.platform.startswith('java'):
|
||||
skipped_extensions = ('.pyc', '.pyo', '.class')
|
||||
if sys.platform.startswith("java"):
|
||||
skipped_extensions = (".pyc", ".pyo", ".class")
|
||||
else:
|
||||
skipped_extensions = ('.pyc', '.pyo')
|
||||
skipped_extensions = (".pyc", ".pyo")
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.loader = getattr(module, '__loader__', None)
|
||||
self.base = os.path.dirname(getattr(module, '__file__', ''))
|
||||
self.loader = getattr(module, "__loader__", None)
|
||||
self.base = os.path.dirname(getattr(module, "__file__", ""))
|
||||
|
||||
def _adjust_path(self, path):
|
||||
return os.path.realpath(path)
|
||||
@ -137,9 +138,9 @@ class ResourceFinder(object):
|
||||
# Issue #50: need to preserve type of path on Python 2.x
|
||||
# like os.path._get_sep
|
||||
if isinstance(resource_name, bytes): # should only happen on 2.x
|
||||
sep = b'/'
|
||||
sep = b"/"
|
||||
else:
|
||||
sep = '/'
|
||||
sep = "/"
|
||||
parts = resource_name.split(sep)
|
||||
parts.insert(0, self.base)
|
||||
result = os.path.join(*parts)
|
||||
@ -164,10 +165,10 @@ class ResourceFinder(object):
|
||||
return result
|
||||
|
||||
def get_stream(self, resource):
|
||||
return open(resource.path, 'rb')
|
||||
return open(resource.path, "rb")
|
||||
|
||||
def get_bytes(self, resource):
|
||||
with open(resource.path, 'rb') as f:
|
||||
with open(resource.path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
def get_size(self, resource):
|
||||
@ -175,8 +176,8 @@ class ResourceFinder(object):
|
||||
|
||||
def get_resources(self, resource):
|
||||
def allowed(f):
|
||||
return (f != '__pycache__' and not
|
||||
f.endswith(self.skipped_extensions))
|
||||
return f != "__pycache__" and not f.endswith(self.skipped_extensions)
|
||||
|
||||
return set([f for f in os.listdir(resource.path) if allowed(f)])
|
||||
|
||||
def is_container(self, resource):
|
||||
@ -197,7 +198,7 @@ class ResourceFinder(object):
|
||||
if not rname:
|
||||
new_name = name
|
||||
else:
|
||||
new_name = '/'.join([rname, name])
|
||||
new_name = "/".join([rname, name])
|
||||
child = self.find(new_name)
|
||||
if child.is_container:
|
||||
todo.append(child)
|
||||
@ -209,12 +210,13 @@ class ZipResourceFinder(ResourceFinder):
|
||||
"""
|
||||
Resource finder for resources in .zip files.
|
||||
"""
|
||||
|
||||
def __init__(self, module):
|
||||
super(ZipResourceFinder, self).__init__(module)
|
||||
archive = self.loader.archive
|
||||
self.prefix_len = 1 + len(archive)
|
||||
# PyPy doesn't have a _files attr on zipimporter, and you can't set one
|
||||
if hasattr(self.loader, '_files'):
|
||||
if hasattr(self.loader, "_files"):
|
||||
self._files = self.loader._files
|
||||
else:
|
||||
self._files = zipimport._zip_directory_cache[archive]
|
||||
@ -224,7 +226,7 @@ class ZipResourceFinder(ResourceFinder):
|
||||
return path
|
||||
|
||||
def _find(self, path):
|
||||
path = path[self.prefix_len:]
|
||||
path = path[self.prefix_len :]
|
||||
if path in self._files:
|
||||
result = True
|
||||
else:
|
||||
@ -236,14 +238,14 @@ class ZipResourceFinder(ResourceFinder):
|
||||
except IndexError:
|
||||
result = False
|
||||
if not result:
|
||||
logger.debug('_find failed: %r %r', path, self.loader.prefix)
|
||||
logger.debug("_find failed: %r %r", path, self.loader.prefix)
|
||||
else:
|
||||
logger.debug('_find worked: %r %r', path, self.loader.prefix)
|
||||
logger.debug("_find worked: %r %r", path, self.loader.prefix)
|
||||
return result
|
||||
|
||||
def get_cache_info(self, resource):
|
||||
prefix = self.loader.archive
|
||||
path = resource.path[1 + len(prefix):]
|
||||
path = resource.path[1 + len(prefix) :]
|
||||
return prefix, path
|
||||
|
||||
def get_bytes(self, resource):
|
||||
@ -253,11 +255,11 @@ class ZipResourceFinder(ResourceFinder):
|
||||
return io.BytesIO(self.get_bytes(resource))
|
||||
|
||||
def get_size(self, resource):
|
||||
path = resource.path[self.prefix_len:]
|
||||
path = resource.path[self.prefix_len :]
|
||||
return self._files[path][3]
|
||||
|
||||
def get_resources(self, resource):
|
||||
path = resource.path[self.prefix_len:]
|
||||
path = resource.path[self.prefix_len :]
|
||||
if path and path[-1] != os.sep:
|
||||
path += os.sep
|
||||
plen = len(path)
|
||||
@ -272,7 +274,7 @@ class ZipResourceFinder(ResourceFinder):
|
||||
return result
|
||||
|
||||
def _is_directory(self, path):
|
||||
path = path[self.prefix_len:]
|
||||
path = path[self.prefix_len :]
|
||||
if path and path[-1] != os.sep:
|
||||
path += os.sep
|
||||
i = bisect.bisect(self.index, path)
|
||||
@ -285,7 +287,7 @@ class ZipResourceFinder(ResourceFinder):
|
||||
|
||||
_finder_registry = {
|
||||
type(None): ResourceFinder,
|
||||
zipimport.zipimporter: ZipResourceFinder
|
||||
zipimport.zipimporter: ZipResourceFinder,
|
||||
}
|
||||
|
||||
try:
|
||||
@ -322,20 +324,21 @@ def finder(package):
|
||||
if package not in sys.modules:
|
||||
__import__(package)
|
||||
module = sys.modules[package]
|
||||
path = getattr(module, '__path__', None)
|
||||
path = getattr(module, "__path__", None)
|
||||
if path is None:
|
||||
raise DistlibException('You cannot get a finder for a module, '
|
||||
'only for a package')
|
||||
loader = getattr(module, '__loader__', None)
|
||||
raise DistlibException(
|
||||
"You cannot get a finder for a module, " "only for a package"
|
||||
)
|
||||
loader = getattr(module, "__loader__", None)
|
||||
finder_maker = _finder_registry.get(type(loader))
|
||||
if finder_maker is None:
|
||||
raise DistlibException('Unable to locate finder for %r' % package)
|
||||
raise DistlibException("Unable to locate finder for %r" % package)
|
||||
result = finder_maker(module)
|
||||
_finder_cache[package] = result
|
||||
return result
|
||||
|
||||
|
||||
_dummy_module = types.ModuleType(str('__dummy__'))
|
||||
_dummy_module = types.ModuleType(str("__dummy__"))
|
||||
|
||||
|
||||
def finder_for_path(path):
|
||||
@ -352,7 +355,7 @@ def finder_for_path(path):
|
||||
finder = _finder_registry.get(type(loader))
|
||||
if finder:
|
||||
module = _dummy_module
|
||||
module.__file__ = os.path.join(path, '')
|
||||
module.__file__ = os.path.join(path, "")
|
||||
module.__loader__ = loader
|
||||
result = finder(module)
|
||||
return result
|
||||
|
||||
@ -15,11 +15,18 @@ from zipfile import ZipInfo
|
||||
|
||||
from .compat import sysconfig, detect_encoding, ZipFile
|
||||
from .resources import finder
|
||||
from .util import (FileOperator, get_export_entry, convert_path, get_executable, get_platform, in_venv)
|
||||
from .util import (
|
||||
FileOperator,
|
||||
get_export_entry,
|
||||
convert_path,
|
||||
get_executable,
|
||||
get_platform,
|
||||
in_venv,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_MANIFEST = '''
|
||||
_DEFAULT_MANIFEST = """
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0"
|
||||
@ -35,18 +42,18 @@ _DEFAULT_MANIFEST = '''
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>'''.strip()
|
||||
</assembly>""".strip()
|
||||
|
||||
# check if Python is called on the first line with this expression
|
||||
FIRST_LINE_RE = re.compile(b'^#!.*pythonw?[0-9.]*([ \t].*)?$')
|
||||
SCRIPT_TEMPLATE = r'''# -*- coding: utf-8 -*-
|
||||
FIRST_LINE_RE = re.compile(b"^#!.*pythonw?[0-9.]*([ \t].*)?$")
|
||||
SCRIPT_TEMPLATE = r"""# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
if __name__ == '__main__':
|
||||
from %(module)s import %(import_name)s
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(%(func)s())
|
||||
'''
|
||||
"""
|
||||
|
||||
# Pre-fetch the contents of all executable wrapper stubs.
|
||||
# This is to address https://github.com/pypa/pip/issues/12666.
|
||||
@ -56,10 +63,10 @@ if __name__ == '__main__':
|
||||
# location where it was imported from. So we load everything into memory in
|
||||
# advance.
|
||||
|
||||
if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'):
|
||||
if os.name == "nt" or (os.name == "java" and os._name == "nt"):
|
||||
# Issue 31: don't hardcode an absolute package name, but
|
||||
# determine it relative to the current package
|
||||
DISTLIB_PACKAGE = __name__.rsplit('.', 1)[0]
|
||||
DISTLIB_PACKAGE = __name__.rsplit(".", 1)[0]
|
||||
|
||||
WRAPPERS = {
|
||||
r.name: r.bytes
|
||||
@ -69,14 +76,14 @@ if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'):
|
||||
|
||||
|
||||
def enquote_executable(executable):
|
||||
if ' ' in executable:
|
||||
if " " in executable:
|
||||
# make sure we quote only the executable in case of env
|
||||
# for example /usr/bin/env "/dir with spaces/bin/jython"
|
||||
# instead of "/usr/bin/env /dir with spaces/bin/jython"
|
||||
# otherwise whole
|
||||
if executable.startswith('/usr/bin/env '):
|
||||
env, _executable = executable.split(' ', 1)
|
||||
if ' ' in _executable and not _executable.startswith('"'):
|
||||
if executable.startswith("/usr/bin/env "):
|
||||
env, _executable = executable.split(" ", 1)
|
||||
if " " in _executable and not _executable.startswith('"'):
|
||||
executable = '%s "%s"' % (env, _executable)
|
||||
else:
|
||||
if not executable.startswith('"'):
|
||||
@ -93,32 +100,37 @@ class ScriptMaker(object):
|
||||
A class to copy or create scripts from source scripts or callable
|
||||
specifications.
|
||||
"""
|
||||
|
||||
script_template = SCRIPT_TEMPLATE
|
||||
|
||||
executable = None # for shebangs
|
||||
|
||||
def __init__(self, source_dir, target_dir, add_launchers=True, dry_run=False, fileop=None):
|
||||
def __init__(
|
||||
self, source_dir, target_dir, add_launchers=True, dry_run=False, fileop=None
|
||||
):
|
||||
self.source_dir = source_dir
|
||||
self.target_dir = target_dir
|
||||
self.add_launchers = add_launchers
|
||||
self.force = False
|
||||
self.clobber = False
|
||||
# It only makes sense to set mode bits on POSIX.
|
||||
self.set_mode = (os.name == 'posix') or (os.name == 'java' and os._name == 'posix')
|
||||
self.variants = set(('', 'X.Y'))
|
||||
self.set_mode = (os.name == "posix") or (
|
||||
os.name == "java" and os._name == "posix"
|
||||
)
|
||||
self.variants = set(("", "X.Y"))
|
||||
self._fileop = fileop or FileOperator(dry_run)
|
||||
|
||||
self._is_nt = os.name == 'nt' or (os.name == 'java' and os._name == 'nt')
|
||||
self._is_nt = os.name == "nt" or (os.name == "java" and os._name == "nt")
|
||||
self.version_info = sys.version_info
|
||||
|
||||
def _get_alternate_executable(self, executable, options):
|
||||
if options.get('gui', False) and self._is_nt: # pragma: no cover
|
||||
if options.get("gui", False) and self._is_nt: # pragma: no cover
|
||||
dn, fn = os.path.split(executable)
|
||||
fn = fn.replace('python', 'pythonw')
|
||||
fn = fn.replace("python", "pythonw")
|
||||
executable = os.path.join(dn, fn)
|
||||
return executable
|
||||
|
||||
if sys.platform.startswith('java'): # pragma: no cover
|
||||
if sys.platform.startswith("java"): # pragma: no cover
|
||||
|
||||
def _is_shell(self, executable):
|
||||
"""
|
||||
@ -127,9 +139,9 @@ class ScriptMaker(object):
|
||||
"""
|
||||
try:
|
||||
with open(executable) as fp:
|
||||
return fp.read(2) == '#!'
|
||||
return fp.read(2) == "#!"
|
||||
except (OSError, IOError):
|
||||
logger.warning('Failed to open %s', executable)
|
||||
logger.warning("Failed to open %s", executable)
|
||||
return False
|
||||
|
||||
def _fix_jython_executable(self, executable):
|
||||
@ -137,12 +149,12 @@ class ScriptMaker(object):
|
||||
# Workaround for Jython is not needed on Linux systems.
|
||||
import java
|
||||
|
||||
if java.lang.System.getProperty('os.name') == 'Linux':
|
||||
if java.lang.System.getProperty("os.name") == "Linux":
|
||||
return executable
|
||||
elif executable.lower().endswith('jython.exe'):
|
||||
elif executable.lower().endswith("jython.exe"):
|
||||
# Use wrapper exe for Jython on Windows
|
||||
return executable
|
||||
return '/usr/bin/env %s' % executable
|
||||
return "/usr/bin/env %s" % executable
|
||||
|
||||
def _build_shebang(self, executable, post_interp):
|
||||
"""
|
||||
@ -155,7 +167,7 @@ class ScriptMaker(object):
|
||||
See also: http://www.in-ulm.de/~mascheck/various/shebang/#length
|
||||
https://hg.mozilla.org/mozilla-central/file/tip/mach
|
||||
"""
|
||||
if os.name != 'posix':
|
||||
if os.name != "posix":
|
||||
simple_shebang = True
|
||||
elif getattr(sys, "cross_compiling", False):
|
||||
# In a cross-compiling environment, the shebang will likely be a
|
||||
@ -166,21 +178,23 @@ class ScriptMaker(object):
|
||||
else:
|
||||
# Add 3 for '#!' prefix and newline suffix.
|
||||
shebang_length = len(executable) + len(post_interp) + 3
|
||||
if sys.platform == 'darwin':
|
||||
if sys.platform == "darwin":
|
||||
max_shebang_length = 512
|
||||
else:
|
||||
max_shebang_length = 127
|
||||
simple_shebang = ((b' ' not in executable) and (shebang_length <= max_shebang_length))
|
||||
simple_shebang = (b" " not in executable) and (
|
||||
shebang_length <= max_shebang_length
|
||||
)
|
||||
|
||||
if simple_shebang:
|
||||
result = b'#!' + executable + post_interp + b'\n'
|
||||
result = b"#!" + executable + post_interp + b"\n"
|
||||
else:
|
||||
result = b'#!/bin/sh\n'
|
||||
result = b"#!/bin/sh\n"
|
||||
result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n'
|
||||
result += b"' '''\n"
|
||||
return result
|
||||
|
||||
def _get_shebang(self, encoding, post_interp=b'', options=None):
|
||||
def _get_shebang(self, encoding, post_interp=b"", options=None):
|
||||
enquote = True
|
||||
if self.executable:
|
||||
executable = self.executable
|
||||
@ -188,21 +202,31 @@ class ScriptMaker(object):
|
||||
elif not sysconfig.is_python_build():
|
||||
executable = get_executable()
|
||||
elif in_venv(): # pragma: no cover
|
||||
executable = os.path.join(sysconfig.get_path('scripts'), 'python%s' % sysconfig.get_config_var('EXE'))
|
||||
executable = os.path.join(
|
||||
sysconfig.get_path("scripts"),
|
||||
"python%s" % sysconfig.get_config_var("EXE"),
|
||||
)
|
||||
else: # pragma: no cover
|
||||
if os.name == 'nt':
|
||||
if os.name == "nt":
|
||||
# for Python builds from source on Windows, no Python executables with
|
||||
# a version suffix are created, so we use python.exe
|
||||
executable = os.path.join(sysconfig.get_config_var('BINDIR'),
|
||||
'python%s' % (sysconfig.get_config_var('EXE')))
|
||||
executable = os.path.join(
|
||||
sysconfig.get_config_var("BINDIR"),
|
||||
"python%s" % (sysconfig.get_config_var("EXE")),
|
||||
)
|
||||
else:
|
||||
executable = os.path.join(
|
||||
sysconfig.get_config_var('BINDIR'),
|
||||
'python%s%s' % (sysconfig.get_config_var('VERSION'), sysconfig.get_config_var('EXE')))
|
||||
sysconfig.get_config_var("BINDIR"),
|
||||
"python%s%s"
|
||||
% (
|
||||
sysconfig.get_config_var("VERSION"),
|
||||
sysconfig.get_config_var("EXE"),
|
||||
),
|
||||
)
|
||||
if options:
|
||||
executable = self._get_alternate_executable(executable, options)
|
||||
|
||||
if sys.platform.startswith('java'): # pragma: no cover
|
||||
if sys.platform.startswith("java"): # pragma: no cover
|
||||
executable = self._fix_jython_executable(executable)
|
||||
|
||||
# Normalise case for Windows - COMMENTED OUT
|
||||
@ -220,11 +244,14 @@ class ScriptMaker(object):
|
||||
executable = enquote_executable(executable)
|
||||
# Issue #51: don't use fsencode, since we later try to
|
||||
# check that the shebang is decodable using utf-8.
|
||||
executable = executable.encode('utf-8')
|
||||
executable = executable.encode("utf-8")
|
||||
# in case of IronPython, play safe and enable frames support
|
||||
if (sys.platform == 'cli' and '-X:Frames' not in post_interp and
|
||||
'-X:FullFrames' not in post_interp): # pragma: no cover
|
||||
post_interp += b' -X:Frames'
|
||||
if (
|
||||
sys.platform == "cli"
|
||||
and "-X:Frames" not in post_interp
|
||||
and "-X:FullFrames" not in post_interp
|
||||
): # pragma: no cover
|
||||
post_interp += b" -X:Frames"
|
||||
shebang = self._build_shebang(executable, post_interp)
|
||||
# Python parser starts to read a script using UTF-8 until
|
||||
# it gets a #coding:xxx cookie. The shebang has to be the
|
||||
@ -232,23 +259,28 @@ class ScriptMaker(object):
|
||||
# written before. So the shebang has to be decodable from
|
||||
# UTF-8.
|
||||
try:
|
||||
shebang.decode('utf-8')
|
||||
shebang.decode("utf-8")
|
||||
except UnicodeDecodeError: # pragma: no cover
|
||||
raise ValueError('The shebang (%r) is not decodable from utf-8' % shebang)
|
||||
raise ValueError("The shebang (%r) is not decodable from utf-8" % shebang)
|
||||
# If the script is encoded to a custom encoding (use a
|
||||
# #coding:xxx cookie), the shebang has to be decodable from
|
||||
# the script encoding too.
|
||||
if encoding != 'utf-8':
|
||||
if encoding != "utf-8":
|
||||
try:
|
||||
shebang.decode(encoding)
|
||||
except UnicodeDecodeError: # pragma: no cover
|
||||
raise ValueError('The shebang (%r) is not decodable '
|
||||
'from the script encoding (%r)' % (shebang, encoding))
|
||||
raise ValueError(
|
||||
"The shebang (%r) is not decodable "
|
||||
"from the script encoding (%r)" % (shebang, encoding)
|
||||
)
|
||||
return shebang
|
||||
|
||||
def _get_script_text(self, entry):
|
||||
return self.script_template % dict(
|
||||
module=entry.prefix, import_name=entry.suffix.split('.')[0], func=entry.suffix)
|
||||
module=entry.prefix,
|
||||
import_name=entry.suffix.split(".")[0],
|
||||
func=entry.suffix,
|
||||
)
|
||||
|
||||
manifest = _DEFAULT_MANIFEST
|
||||
|
||||
@ -261,82 +293,90 @@ class ScriptMaker(object):
|
||||
if not use_launcher:
|
||||
script_bytes = shebang + script_bytes
|
||||
else: # pragma: no cover
|
||||
if ext == 'py':
|
||||
launcher = self._get_launcher('t')
|
||||
if ext == "py":
|
||||
launcher = self._get_launcher("t")
|
||||
else:
|
||||
launcher = self._get_launcher('w')
|
||||
launcher = self._get_launcher("w")
|
||||
stream = BytesIO()
|
||||
with ZipFile(stream, 'w') as zf:
|
||||
source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
|
||||
with ZipFile(stream, "w") as zf:
|
||||
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH")
|
||||
if source_date_epoch:
|
||||
date_time = time.gmtime(int(source_date_epoch))[:6]
|
||||
zinfo = ZipInfo(filename='__main__.py', date_time=date_time)
|
||||
zinfo = ZipInfo(filename="__main__.py", date_time=date_time)
|
||||
zf.writestr(zinfo, script_bytes)
|
||||
else:
|
||||
zf.writestr('__main__.py', script_bytes)
|
||||
zf.writestr("__main__.py", script_bytes)
|
||||
zip_data = stream.getvalue()
|
||||
script_bytes = launcher + shebang + zip_data
|
||||
for name in names:
|
||||
outname = os.path.join(self.target_dir, name)
|
||||
if use_launcher: # pragma: no cover
|
||||
n, e = os.path.splitext(outname)
|
||||
if e.startswith('.py'):
|
||||
if e.startswith(".py"):
|
||||
outname = n
|
||||
outname = '%s.exe' % outname
|
||||
outname = "%s.exe" % outname
|
||||
try:
|
||||
self._fileop.write_binary_file(outname, script_bytes)
|
||||
except Exception:
|
||||
# Failed writing an executable - it might be in use.
|
||||
logger.warning('Failed to write executable - trying to '
|
||||
'use .deleteme logic')
|
||||
dfname = '%s.deleteme' % outname
|
||||
logger.warning(
|
||||
"Failed to write executable - trying to " "use .deleteme logic"
|
||||
)
|
||||
dfname = "%s.deleteme" % outname
|
||||
if os.path.exists(dfname):
|
||||
os.remove(dfname) # Not allowed to fail here
|
||||
os.rename(outname, dfname) # nor here
|
||||
self._fileop.write_binary_file(outname, script_bytes)
|
||||
logger.debug('Able to replace executable using '
|
||||
'.deleteme logic')
|
||||
logger.debug("Able to replace executable using " ".deleteme logic")
|
||||
try:
|
||||
os.remove(dfname)
|
||||
except Exception:
|
||||
pass # still in use - ignore error
|
||||
else:
|
||||
if self._is_nt and not outname.endswith('.' + ext): # pragma: no cover
|
||||
outname = '%s.%s' % (outname, ext)
|
||||
if self._is_nt and not outname.endswith("." + ext): # pragma: no cover
|
||||
outname = "%s.%s" % (outname, ext)
|
||||
if os.path.exists(outname) and not self.clobber:
|
||||
logger.warning('Skipping existing file %s', outname)
|
||||
logger.warning("Skipping existing file %s", outname)
|
||||
continue
|
||||
self._fileop.write_binary_file(outname, script_bytes)
|
||||
if self.set_mode:
|
||||
self._fileop.set_executable_mode([outname])
|
||||
filenames.append(outname)
|
||||
|
||||
variant_separator = '-'
|
||||
variant_separator = "-"
|
||||
|
||||
def get_script_filenames(self, name):
|
||||
result = set()
|
||||
if '' in self.variants:
|
||||
if "" in self.variants:
|
||||
result.add(name)
|
||||
if 'X' in self.variants:
|
||||
result.add('%s%s' % (name, self.version_info[0]))
|
||||
if 'X.Y' in self.variants:
|
||||
result.add('%s%s%s.%s' % (name, self.variant_separator, self.version_info[0], self.version_info[1]))
|
||||
if "X" in self.variants:
|
||||
result.add("%s%s" % (name, self.version_info[0]))
|
||||
if "X.Y" in self.variants:
|
||||
result.add(
|
||||
"%s%s%s.%s"
|
||||
% (
|
||||
name,
|
||||
self.variant_separator,
|
||||
self.version_info[0],
|
||||
self.version_info[1],
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def _make_script(self, entry, filenames, options=None):
|
||||
post_interp = b''
|
||||
post_interp = b""
|
||||
if options:
|
||||
args = options.get('interpreter_args', [])
|
||||
args = options.get("interpreter_args", [])
|
||||
if args:
|
||||
args = ' %s' % ' '.join(args)
|
||||
post_interp = args.encode('utf-8')
|
||||
shebang = self._get_shebang('utf-8', post_interp, options=options)
|
||||
script = self._get_script_text(entry).encode('utf-8')
|
||||
args = " %s" % " ".join(args)
|
||||
post_interp = args.encode("utf-8")
|
||||
shebang = self._get_shebang("utf-8", post_interp, options=options)
|
||||
script = self._get_script_text(entry).encode("utf-8")
|
||||
scriptnames = self.get_script_filenames(entry.name)
|
||||
if options and options.get('gui', False):
|
||||
ext = 'pyw'
|
||||
if options and options.get("gui", False):
|
||||
ext = "pyw"
|
||||
else:
|
||||
ext = 'py'
|
||||
ext = "py"
|
||||
self._write_script(scriptnames, shebang, script, filenames, ext)
|
||||
|
||||
def _copy_script(self, script, filenames):
|
||||
@ -344,14 +384,14 @@ class ScriptMaker(object):
|
||||
script = os.path.join(self.source_dir, convert_path(script))
|
||||
outname = os.path.join(self.target_dir, os.path.basename(script))
|
||||
if not self.force and not self._fileop.newer(script, outname):
|
||||
logger.debug('not copying %s (up-to-date)', script)
|
||||
logger.debug("not copying %s (up-to-date)", script)
|
||||
return
|
||||
|
||||
# Always open the file, but ignore failures in dry-run mode --
|
||||
# that way, we'll get accurate feedback if we can read the
|
||||
# script.
|
||||
try:
|
||||
f = open(script, 'rb')
|
||||
f = open(script, "rb")
|
||||
except IOError: # pragma: no cover
|
||||
if not self.dry_run:
|
||||
raise
|
||||
@ -359,13 +399,13 @@ class ScriptMaker(object):
|
||||
else:
|
||||
first_line = f.readline()
|
||||
if not first_line: # pragma: no cover
|
||||
logger.warning('%s is an empty file (skipping)', script)
|
||||
logger.warning("%s is an empty file (skipping)", script)
|
||||
return
|
||||
|
||||
match = FIRST_LINE_RE.match(first_line.replace(b'\r\n', b'\n'))
|
||||
match = FIRST_LINE_RE.match(first_line.replace(b"\r\n", b"\n"))
|
||||
if match:
|
||||
adjust = True
|
||||
post_interp = match.group(1) or b''
|
||||
post_interp = match.group(1) or b""
|
||||
|
||||
if not adjust:
|
||||
if f:
|
||||
@ -375,15 +415,15 @@ class ScriptMaker(object):
|
||||
self._fileop.set_executable_mode([outname])
|
||||
filenames.append(outname)
|
||||
else:
|
||||
logger.info('copying and adjusting %s -> %s', script, self.target_dir)
|
||||
logger.info("copying and adjusting %s -> %s", script, self.target_dir)
|
||||
if not self._fileop.dry_run:
|
||||
encoding, lines = detect_encoding(f.readline)
|
||||
f.seek(0)
|
||||
shebang = self._get_shebang(encoding, post_interp)
|
||||
if b'pythonw' in first_line: # pragma: no cover
|
||||
ext = 'pyw'
|
||||
if b"pythonw" in first_line: # pragma: no cover
|
||||
ext = "pyw"
|
||||
else:
|
||||
ext = 'py'
|
||||
ext = "py"
|
||||
n = os.path.basename(outname)
|
||||
self._write_script([n], shebang, f.read(), filenames, ext)
|
||||
if f:
|
||||
@ -397,20 +437,22 @@ class ScriptMaker(object):
|
||||
def dry_run(self, value):
|
||||
self._fileop.dry_run = value
|
||||
|
||||
if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'): # pragma: no cover
|
||||
if os.name == "nt" or (os.name == "java" and os._name == "nt"): # pragma: no cover
|
||||
# Executable launcher support.
|
||||
# Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
|
||||
|
||||
def _get_launcher(self, kind):
|
||||
if struct.calcsize('P') == 8: # 64-bit
|
||||
bits = '64'
|
||||
if struct.calcsize("P") == 8: # 64-bit
|
||||
bits = "64"
|
||||
else:
|
||||
bits = '32'
|
||||
platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
|
||||
name = '%s%s%s.exe' % (kind, bits, platform_suffix)
|
||||
bits = "32"
|
||||
platform_suffix = "-arm" if get_platform() == "win-arm64" else ""
|
||||
name = "%s%s%s.exe" % (kind, bits, platform_suffix)
|
||||
if name not in WRAPPERS:
|
||||
msg = ('Unable to find resource %s in package %s' %
|
||||
(name, DISTLIB_PACKAGE))
|
||||
msg = "Unable to find resource %s in package %s" % (
|
||||
name,
|
||||
DISTLIB_PACKAGE,
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return WRAPPERS[name]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -14,16 +14,23 @@ import re
|
||||
from .compat import string_types
|
||||
from .util import parse_requirement
|
||||
|
||||
__all__ = ['NormalizedVersion', 'NormalizedMatcher',
|
||||
'LegacyVersion', 'LegacyMatcher',
|
||||
'SemanticVersion', 'SemanticMatcher',
|
||||
'UnsupportedVersionError', 'get_scheme']
|
||||
__all__ = [
|
||||
"NormalizedVersion",
|
||||
"NormalizedMatcher",
|
||||
"LegacyVersion",
|
||||
"LegacyMatcher",
|
||||
"SemanticVersion",
|
||||
"SemanticMatcher",
|
||||
"UnsupportedVersionError",
|
||||
"get_scheme",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnsupportedVersionError(ValueError):
|
||||
"""This is an unsupported version."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -35,11 +42,11 @@ class Version(object):
|
||||
assert len(parts) > 0
|
||||
|
||||
def parse(self, s):
|
||||
raise NotImplementedError('please implement in a subclass')
|
||||
raise NotImplementedError("please implement in a subclass")
|
||||
|
||||
def _check_compatible(self, other):
|
||||
if type(self) != type(other):
|
||||
raise TypeError('cannot compare %r and %r' % (self, other))
|
||||
raise TypeError("cannot compare %r and %r" % (self, other))
|
||||
|
||||
def __eq__(self, other):
|
||||
self._check_compatible(other)
|
||||
@ -73,7 +80,7 @@ class Version(object):
|
||||
|
||||
@property
|
||||
def is_prerelease(self):
|
||||
raise NotImplementedError('Please implement in subclasses.')
|
||||
raise NotImplementedError("Please implement in subclasses.")
|
||||
|
||||
|
||||
class Matcher(object):
|
||||
@ -81,15 +88,15 @@ class Matcher(object):
|
||||
|
||||
# value is either a callable or the name of a method
|
||||
_operators = {
|
||||
'<': lambda v, c, p: v < c,
|
||||
'>': lambda v, c, p: v > c,
|
||||
'<=': lambda v, c, p: v == c or v < c,
|
||||
'>=': lambda v, c, p: v == c or v > c,
|
||||
'==': lambda v, c, p: v == c,
|
||||
'===': lambda v, c, p: v == c,
|
||||
"<": lambda v, c, p: v < c,
|
||||
">": lambda v, c, p: v > c,
|
||||
"<=": lambda v, c, p: v == c or v < c,
|
||||
">=": lambda v, c, p: v == c or v > c,
|
||||
"==": lambda v, c, p: v == c,
|
||||
"===": lambda v, c, p: v == c,
|
||||
# by default, compatible => >=.
|
||||
'~=': lambda v, c, p: v == c or v > c,
|
||||
'!=': lambda v, c, p: v != c,
|
||||
"~=": lambda v, c, p: v == c or v > c,
|
||||
"!=": lambda v, c, p: v != c,
|
||||
}
|
||||
|
||||
# this is a method only to support alternative implementations
|
||||
@ -99,21 +106,20 @@ class Matcher(object):
|
||||
|
||||
def __init__(self, s):
|
||||
if self.version_class is None:
|
||||
raise ValueError('Please specify a version class')
|
||||
raise ValueError("Please specify a version class")
|
||||
self._string = s = s.strip()
|
||||
r = self.parse_requirement(s)
|
||||
if not r:
|
||||
raise ValueError('Not valid: %r' % s)
|
||||
raise ValueError("Not valid: %r" % s)
|
||||
self.name = r.name
|
||||
self.key = self.name.lower() # for case-insensitive comparisons
|
||||
clist = []
|
||||
if r.constraints:
|
||||
# import pdb; pdb.set_trace()
|
||||
for op, s in r.constraints:
|
||||
if s.endswith('.*'):
|
||||
if op not in ('==', '!='):
|
||||
raise ValueError('\'.*\' not allowed for '
|
||||
'%r constraints' % op)
|
||||
if s.endswith(".*"):
|
||||
if op not in ("==", "!="):
|
||||
raise ValueError("'.*' not allowed for " "%r constraints" % op)
|
||||
# Could be a partial version (e.g. for '2.*') which
|
||||
# won't parse as a version, so keep it as a string
|
||||
vn, prefix = s[:-2], True
|
||||
@ -140,8 +146,10 @@ class Matcher(object):
|
||||
if isinstance(f, string_types):
|
||||
f = getattr(self, f)
|
||||
if not f:
|
||||
msg = ('%r not implemented '
|
||||
'for %s' % (operator, self.__class__.__name__))
|
||||
msg = "%r not implemented " "for %s" % (
|
||||
operator,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
if not f(version, constraint, prefix):
|
||||
return False
|
||||
@ -150,13 +158,13 @@ class Matcher(object):
|
||||
@property
|
||||
def exact_version(self):
|
||||
result = None
|
||||
if len(self._parts) == 1 and self._parts[0][0] in ('==', '==='):
|
||||
if len(self._parts) == 1 and self._parts[0][0] in ("==", "==="):
|
||||
result = self._parts[0][1]
|
||||
return result
|
||||
|
||||
def _check_compatible(self, other):
|
||||
if type(self) != type(other) or self.name != other.name:
|
||||
raise TypeError('cannot compare %s and %s' % (self, other))
|
||||
raise TypeError("cannot compare %s and %s" % (self, other))
|
||||
|
||||
def __eq__(self, other):
|
||||
self._check_compatible(other)
|
||||
@ -176,18 +184,21 @@ class Matcher(object):
|
||||
return self._string
|
||||
|
||||
|
||||
PEP440_VERSION_RE = re.compile(r'^v?(\d+!)?(\d+(\.\d+)*)((a|alpha|b|beta|c|rc|pre|preview)(\d+)?)?'
|
||||
r'(\.(post|r|rev)(\d+)?)?([._-]?(dev)(\d+)?)?'
|
||||
r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$', re.I)
|
||||
PEP440_VERSION_RE = re.compile(
|
||||
r"^v?(\d+!)?(\d+(\.\d+)*)((a|alpha|b|beta|c|rc|pre|preview)(\d+)?)?"
|
||||
r"(\.(post|r|rev)(\d+)?)?([._-]?(dev)(\d+)?)?"
|
||||
r"(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def _pep_440_key(s):
|
||||
s = s.strip()
|
||||
m = PEP440_VERSION_RE.match(s)
|
||||
if not m:
|
||||
raise UnsupportedVersionError('Not a valid version: %s' % s)
|
||||
raise UnsupportedVersionError("Not a valid version: %s" % s)
|
||||
groups = m.groups()
|
||||
nums = tuple(int(v) for v in groups[1].split('.'))
|
||||
nums = tuple(int(v) for v in groups[1].split("."))
|
||||
while len(nums) > 1 and nums[-1] == 0:
|
||||
nums = nums[:-1]
|
||||
|
||||
@ -224,7 +235,7 @@ def _pep_440_key(s):
|
||||
local = ()
|
||||
else:
|
||||
parts = []
|
||||
for part in local.split('.'):
|
||||
for part in local.split("."):
|
||||
# to ensure that numeric compares as > lexicographic, avoid
|
||||
# comparing them directly, but encode a tuple which ensures
|
||||
# correct sorting
|
||||
@ -238,14 +249,14 @@ def _pep_440_key(s):
|
||||
# either before pre-release, or final release and after
|
||||
if not post and dev:
|
||||
# before pre-release
|
||||
pre = ('a', -1) # to sort before a0
|
||||
pre = ("a", -1) # to sort before a0
|
||||
else:
|
||||
pre = ('z',) # to sort after all pre-releases
|
||||
pre = ("z",) # to sort after all pre-releases
|
||||
# now look at the state of post and dev.
|
||||
if not post:
|
||||
post = ('_',) # sort before 'a'
|
||||
post = ("_",) # sort before 'a'
|
||||
if not dev:
|
||||
dev = ('final',)
|
||||
dev = ("final",)
|
||||
|
||||
return epoch, nums, pre, post, dev, local
|
||||
|
||||
@ -271,6 +282,7 @@ class NormalizedVersion(Version):
|
||||
1.2a # release level must have a release serial
|
||||
1.2.3b
|
||||
"""
|
||||
|
||||
def parse(self, s):
|
||||
result = _normalized_key(s)
|
||||
# _normalized_key loses trailing zeroes in the release
|
||||
@ -279,10 +291,10 @@ class NormalizedVersion(Version):
|
||||
# (~= 1.4.5.0) matches differently to (~= 1.4.5.0.0).
|
||||
m = PEP440_VERSION_RE.match(s) # must succeed
|
||||
groups = m.groups()
|
||||
self._release_clause = tuple(int(v) for v in groups[1].split('.'))
|
||||
self._release_clause = tuple(int(v) for v in groups[1].split("."))
|
||||
return result
|
||||
|
||||
PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
|
||||
PREREL_TAGS = set(["a", "b", "c", "rc", "dev"])
|
||||
|
||||
@property
|
||||
def is_prerelease(self):
|
||||
@ -297,7 +309,7 @@ def _match_prefix(x, y):
|
||||
if not x.startswith(y):
|
||||
return False
|
||||
n = len(y)
|
||||
return x[n] == '.'
|
||||
return x[n] == "."
|
||||
|
||||
|
||||
class NormalizedMatcher(Matcher):
|
||||
@ -305,19 +317,19 @@ class NormalizedMatcher(Matcher):
|
||||
|
||||
# value is either a callable or the name of a method
|
||||
_operators = {
|
||||
'~=': '_match_compatible',
|
||||
'<': '_match_lt',
|
||||
'>': '_match_gt',
|
||||
'<=': '_match_le',
|
||||
'>=': '_match_ge',
|
||||
'==': '_match_eq',
|
||||
'===': '_match_arbitrary',
|
||||
'!=': '_match_ne',
|
||||
"~=": "_match_compatible",
|
||||
"<": "_match_lt",
|
||||
">": "_match_gt",
|
||||
"<=": "_match_le",
|
||||
">=": "_match_ge",
|
||||
"==": "_match_eq",
|
||||
"===": "_match_arbitrary",
|
||||
"!=": "_match_ne",
|
||||
}
|
||||
|
||||
def _adjust_local(self, version, constraint, prefix):
|
||||
if prefix:
|
||||
strip_local = '+' not in constraint and version._parts[-1]
|
||||
strip_local = "+" not in constraint and version._parts[-1]
|
||||
else:
|
||||
# both constraint and version are
|
||||
# NormalizedVersion instances.
|
||||
@ -325,7 +337,7 @@ class NormalizedMatcher(Matcher):
|
||||
# ensure the version doesn't, either.
|
||||
strip_local = not constraint._parts[-1] and version._parts[-1]
|
||||
if strip_local:
|
||||
s = version._string.split('+', 1)[0]
|
||||
s = version._string.split("+", 1)[0]
|
||||
version = self.version_class(s)
|
||||
return version, constraint
|
||||
|
||||
@ -334,7 +346,7 @@ class NormalizedMatcher(Matcher):
|
||||
if version >= constraint:
|
||||
return False
|
||||
release_clause = constraint._release_clause
|
||||
pfx = '.'.join([str(i) for i in release_clause])
|
||||
pfx = ".".join([str(i) for i in release_clause])
|
||||
return not _match_prefix(version, pfx)
|
||||
|
||||
def _match_gt(self, version, constraint, prefix):
|
||||
@ -342,7 +354,7 @@ class NormalizedMatcher(Matcher):
|
||||
if version <= constraint:
|
||||
return False
|
||||
release_clause = constraint._release_clause
|
||||
pfx = '.'.join([str(i) for i in release_clause])
|
||||
pfx = ".".join([str(i) for i in release_clause])
|
||||
return not _match_prefix(version, pfx)
|
||||
|
||||
def _match_le(self, version, constraint, prefix):
|
||||
@ -356,7 +368,7 @@ class NormalizedMatcher(Matcher):
|
||||
def _match_eq(self, version, constraint, prefix):
|
||||
version, constraint = self._adjust_local(version, constraint, prefix)
|
||||
if not prefix:
|
||||
result = (version == constraint)
|
||||
result = version == constraint
|
||||
else:
|
||||
result = _match_prefix(version, constraint)
|
||||
return result
|
||||
@ -367,7 +379,7 @@ class NormalizedMatcher(Matcher):
|
||||
def _match_ne(self, version, constraint, prefix):
|
||||
version, constraint = self._adjust_local(version, constraint, prefix)
|
||||
if not prefix:
|
||||
result = (version != constraint)
|
||||
result = version != constraint
|
||||
else:
|
||||
result = not _match_prefix(version, constraint)
|
||||
return result
|
||||
@ -378,38 +390,37 @@ class NormalizedMatcher(Matcher):
|
||||
return True
|
||||
if version < constraint:
|
||||
return False
|
||||
# if not prefix:
|
||||
# return True
|
||||
# if not prefix:
|
||||
# return True
|
||||
release_clause = constraint._release_clause
|
||||
if len(release_clause) > 1:
|
||||
release_clause = release_clause[:-1]
|
||||
pfx = '.'.join([str(i) for i in release_clause])
|
||||
pfx = ".".join([str(i) for i in release_clause])
|
||||
return _match_prefix(version, pfx)
|
||||
|
||||
|
||||
_REPLACEMENTS = (
|
||||
(re.compile('[.+-]$'), ''), # remove trailing puncts
|
||||
(re.compile(r'^[.](\d)'), r'0.\1'), # .N -> 0.N at start
|
||||
(re.compile('^[.-]'), ''), # remove leading puncts
|
||||
(re.compile(r'^\((.*)\)$'), r'\1'), # remove parentheses
|
||||
(re.compile(r'^v(ersion)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
|
||||
(re.compile(r'^r(ev)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
|
||||
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
|
||||
(re.compile(r'\b(alfa|apha)\b'), 'alpha'), # misspelt alpha
|
||||
(re.compile(r'\b(pre-alpha|prealpha)\b'),
|
||||
'pre.alpha'), # standardise
|
||||
(re.compile(r'\(beta\)$'), 'beta'), # remove parentheses
|
||||
(re.compile("[.+-]$"), ""), # remove trailing puncts
|
||||
(re.compile(r"^[.](\d)"), r"0.\1"), # .N -> 0.N at start
|
||||
(re.compile("^[.-]"), ""), # remove leading puncts
|
||||
(re.compile(r"^\((.*)\)$"), r"\1"), # remove parentheses
|
||||
(re.compile(r"^v(ersion)?\s*(\d+)"), r"\2"), # remove leading v(ersion)
|
||||
(re.compile(r"^r(ev)?\s*(\d+)"), r"\2"), # remove leading v(ersion)
|
||||
(re.compile("[.]{2,}"), "."), # multiple runs of '.'
|
||||
(re.compile(r"\b(alfa|apha)\b"), "alpha"), # misspelt alpha
|
||||
(re.compile(r"\b(pre-alpha|prealpha)\b"), "pre.alpha"), # standardise
|
||||
(re.compile(r"\(beta\)$"), "beta"), # remove parentheses
|
||||
)
|
||||
|
||||
_SUFFIX_REPLACEMENTS = (
|
||||
(re.compile('^[:~._+-]+'), ''), # remove leading puncts
|
||||
(re.compile('[,*")([\\]]'), ''), # remove unwanted chars
|
||||
(re.compile('[~:+_ -]'), '.'), # replace illegal chars
|
||||
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
|
||||
(re.compile(r'\.$'), ''), # trailing '.'
|
||||
(re.compile("^[:~._+-]+"), ""), # remove leading puncts
|
||||
(re.compile('[,*")([\\]]'), ""), # remove unwanted chars
|
||||
(re.compile("[~:+_ -]"), "."), # replace illegal chars
|
||||
(re.compile("[.]{2,}"), "."), # multiple runs of '.'
|
||||
(re.compile(r"\.$"), ""), # trailing '.'
|
||||
)
|
||||
|
||||
_NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)')
|
||||
_NUMERIC_PREFIX = re.compile(r"(\d+(\.\d+)*)")
|
||||
|
||||
|
||||
def _suggest_semantic_version(s):
|
||||
@ -421,26 +432,26 @@ def _suggest_semantic_version(s):
|
||||
for pat, repl in _REPLACEMENTS:
|
||||
result = pat.sub(repl, result)
|
||||
if not result:
|
||||
result = '0.0.0'
|
||||
result = "0.0.0"
|
||||
|
||||
# Now look for numeric prefix, and separate it out from
|
||||
# the rest.
|
||||
# import pdb; pdb.set_trace()
|
||||
m = _NUMERIC_PREFIX.match(result)
|
||||
if not m:
|
||||
prefix = '0.0.0'
|
||||
prefix = "0.0.0"
|
||||
suffix = result
|
||||
else:
|
||||
prefix = m.groups()[0].split('.')
|
||||
prefix = m.groups()[0].split(".")
|
||||
prefix = [int(i) for i in prefix]
|
||||
while len(prefix) < 3:
|
||||
prefix.append(0)
|
||||
if len(prefix) == 3:
|
||||
suffix = result[m.end():]
|
||||
suffix = result[m.end() :]
|
||||
else:
|
||||
suffix = '.'.join([str(i) for i in prefix[3:]]) + result[m.end():]
|
||||
suffix = ".".join([str(i) for i in prefix[3:]]) + result[m.end() :]
|
||||
prefix = prefix[:3]
|
||||
prefix = '.'.join([str(i) for i in prefix])
|
||||
prefix = ".".join([str(i) for i in prefix])
|
||||
suffix = suffix.strip()
|
||||
if suffix:
|
||||
# import pdb; pdb.set_trace()
|
||||
@ -451,7 +462,7 @@ def _suggest_semantic_version(s):
|
||||
if not suffix:
|
||||
result = prefix
|
||||
else:
|
||||
sep = '-' if 'dev' in suffix else '+'
|
||||
sep = "-" if "dev" in suffix else "+"
|
||||
result = prefix + sep + suffix
|
||||
if not is_semver(result):
|
||||
result = None
|
||||
@ -484,12 +495,23 @@ def _suggest_normalized_version(s):
|
||||
rs = s.lower()
|
||||
|
||||
# part of this could use maketrans
|
||||
for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
|
||||
('beta', 'b'), ('rc', 'c'), ('-final', ''),
|
||||
('-pre', 'c'),
|
||||
('-release', ''), ('.release', ''), ('-stable', ''),
|
||||
('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
|
||||
('final', '')):
|
||||
for orig, repl in (
|
||||
("-alpha", "a"),
|
||||
("-beta", "b"),
|
||||
("alpha", "a"),
|
||||
("beta", "b"),
|
||||
("rc", "c"),
|
||||
("-final", ""),
|
||||
("-pre", "c"),
|
||||
("-release", ""),
|
||||
(".release", ""),
|
||||
("-stable", ""),
|
||||
("+", "."),
|
||||
("_", "."),
|
||||
(" ", ""),
|
||||
(".final", ""),
|
||||
("final", ""),
|
||||
):
|
||||
rs = rs.replace(orig, repl)
|
||||
|
||||
# if something ends with dev or pre, we add a 0
|
||||
@ -509,7 +531,7 @@ def _suggest_normalized_version(s):
|
||||
rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
|
||||
|
||||
# Clean: v0.3, v1.0
|
||||
if rs.startswith('v'):
|
||||
if rs.startswith("v"):
|
||||
rs = rs[1:]
|
||||
|
||||
# Clean leading '0's on numbers.
|
||||
@ -568,20 +590,21 @@ def _suggest_normalized_version(s):
|
||||
rs = None
|
||||
return rs
|
||||
|
||||
|
||||
#
|
||||
# Legacy version processing (distribute-compatible)
|
||||
#
|
||||
|
||||
|
||||
_VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
|
||||
_VERSION_PART = re.compile(r"([a-z]+|\d+|[\.-])", re.I)
|
||||
_VERSION_REPLACE = {
|
||||
'pre': 'c',
|
||||
'preview': 'c',
|
||||
'-': 'final-',
|
||||
'rc': 'c',
|
||||
'dev': '@',
|
||||
'': None,
|
||||
'.': None,
|
||||
"pre": "c",
|
||||
"preview": "c",
|
||||
"-": "final-",
|
||||
"rc": "c",
|
||||
"dev": "@",
|
||||
"": None,
|
||||
".": None,
|
||||
}
|
||||
|
||||
|
||||
@ -591,21 +614,21 @@ def _legacy_key(s):
|
||||
for p in _VERSION_PART.split(s.lower()):
|
||||
p = _VERSION_REPLACE.get(p, p)
|
||||
if p:
|
||||
if '0' <= p[:1] <= '9':
|
||||
if "0" <= p[:1] <= "9":
|
||||
p = p.zfill(8)
|
||||
else:
|
||||
p = '*' + p
|
||||
p = "*" + p
|
||||
result.append(p)
|
||||
result.append('*final')
|
||||
result.append("*final")
|
||||
return result
|
||||
|
||||
result = []
|
||||
for p in get_parts(s):
|
||||
if p.startswith('*'):
|
||||
if p < '*final':
|
||||
while result and result[-1] == '*final-':
|
||||
if p.startswith("*"):
|
||||
if p < "*final":
|
||||
while result and result[-1] == "*final-":
|
||||
result.pop()
|
||||
while result and result[-1] == '00000000':
|
||||
while result and result[-1] == "00000000":
|
||||
result.pop()
|
||||
result.append(p)
|
||||
return tuple(result)
|
||||
@ -619,7 +642,7 @@ class LegacyVersion(Version):
|
||||
def is_prerelease(self):
|
||||
result = False
|
||||
for x in self._parts:
|
||||
if (isinstance(x, string_types) and x.startswith('*') and x < '*final'):
|
||||
if isinstance(x, string_types) and x.startswith("*") and x < "*final":
|
||||
result = True
|
||||
break
|
||||
return result
|
||||
@ -629,31 +652,38 @@ class LegacyMatcher(Matcher):
|
||||
version_class = LegacyVersion
|
||||
|
||||
_operators = dict(Matcher._operators)
|
||||
_operators['~='] = '_match_compatible'
|
||||
_operators["~="] = "_match_compatible"
|
||||
|
||||
numeric_re = re.compile(r'^(\d+(\.\d+)*)')
|
||||
numeric_re = re.compile(r"^(\d+(\.\d+)*)")
|
||||
|
||||
def _match_compatible(self, version, constraint, prefix):
|
||||
if version < constraint:
|
||||
return False
|
||||
m = self.numeric_re.match(str(constraint))
|
||||
if not m:
|
||||
logger.warning('Cannot compute compatible match for version %s '
|
||||
' and constraint %s', version, constraint)
|
||||
logger.warning(
|
||||
"Cannot compute compatible match for version %s " " and constraint %s",
|
||||
version,
|
||||
constraint,
|
||||
)
|
||||
return True
|
||||
s = m.groups()[0]
|
||||
if '.' in s:
|
||||
s = s.rsplit('.', 1)[0]
|
||||
if "." in s:
|
||||
s = s.rsplit(".", 1)[0]
|
||||
return _match_prefix(version, s)
|
||||
|
||||
|
||||
#
|
||||
# Semantic versioning
|
||||
#
|
||||
|
||||
|
||||
_SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
|
||||
r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
|
||||
r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
|
||||
_SEMVER_RE = re.compile(
|
||||
r"^(\d+)\.(\d+)\.(\d+)"
|
||||
r"(-[a-z0-9]+(\.[a-z0-9-]+)*)?"
|
||||
r"(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def is_semver(s):
|
||||
@ -665,7 +695,7 @@ def _semantic_key(s):
|
||||
if s is None:
|
||||
result = (absent,)
|
||||
else:
|
||||
parts = s[1:].split('.')
|
||||
parts = s[1:].split(".")
|
||||
# We can't compare ints and strings on Python 3, so fudge it
|
||||
# by zero-filling numeric values so simulate a numeric comparison
|
||||
result = tuple([p.zfill(8) if p.isdigit() else p for p in parts])
|
||||
@ -677,7 +707,7 @@ def _semantic_key(s):
|
||||
groups = m.groups()
|
||||
major, minor, patch = [int(i) for i in groups[:3]]
|
||||
# choose the '|' and '*' so that versions sort correctly
|
||||
pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*')
|
||||
pre, build = make_tuple(groups[3], "|"), make_tuple(groups[5], "*")
|
||||
return (major, minor, patch), pre, build
|
||||
|
||||
|
||||
@ -687,7 +717,7 @@ class SemanticVersion(Version):
|
||||
|
||||
@property
|
||||
def is_prerelease(self):
|
||||
return self._parts[1][0] != '|'
|
||||
return self._parts[1][0] != "|"
|
||||
|
||||
|
||||
class SemanticMatcher(Matcher):
|
||||
@ -721,9 +751,9 @@ class VersionScheme(object):
|
||||
Used for processing some metadata fields
|
||||
"""
|
||||
# See issue #140. Be tolerant of a single trailing comma.
|
||||
if s.endswith(','):
|
||||
if s.endswith(","):
|
||||
s = s[:-1]
|
||||
return self.is_valid_matcher('dummy_name (%s)' % s)
|
||||
return self.is_valid_matcher("dummy_name (%s)" % s)
|
||||
|
||||
def suggest(self, s):
|
||||
if self.suggester is None:
|
||||
@ -734,17 +764,19 @@ class VersionScheme(object):
|
||||
|
||||
|
||||
_SCHEMES = {
|
||||
'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
|
||||
_suggest_normalized_version),
|
||||
'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
|
||||
'semantic': VersionScheme(_semantic_key, SemanticMatcher,
|
||||
_suggest_semantic_version),
|
||||
"normalized": VersionScheme(
|
||||
_normalized_key, NormalizedMatcher, _suggest_normalized_version
|
||||
),
|
||||
"legacy": VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
|
||||
"semantic": VersionScheme(
|
||||
_semantic_key, SemanticMatcher, _suggest_semantic_version
|
||||
),
|
||||
}
|
||||
|
||||
_SCHEMES['default'] = _SCHEMES['normalized']
|
||||
_SCHEMES["default"] = _SCHEMES["normalized"]
|
||||
|
||||
|
||||
def get_scheme(name):
|
||||
if name not in _SCHEMES:
|
||||
raise ValueError('unknown scheme name: %r' % name)
|
||||
raise ValueError("unknown scheme name: %r" % name)
|
||||
return _SCHEMES[name]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,9 @@ from ._error import Timeout
|
||||
|
||||
try:
|
||||
from ._read_write import ReadWriteLock
|
||||
except ImportError: # sqlite3 may be unavailable if Python was built without it or the C library is missing
|
||||
except (
|
||||
ImportError
|
||||
): # sqlite3 may be unavailable if Python was built without it or the C library is missing
|
||||
ReadWriteLock = None # type: ignore[assignment, misc]
|
||||
|
||||
from ._soft import SoftFileLock
|
||||
|
||||
@ -99,7 +99,9 @@ class FileLockContext:
|
||||
lock_file_fd: int | None = None
|
||||
|
||||
#: The lock counter is used for implementing the nested locking mechanism.
|
||||
lock_counter: int = 0 # When the lock is acquired is increased and the lock is only released, when this value is 0
|
||||
lock_counter: int = (
|
||||
0 # When the lock is acquired is increased and the lock is only released, when this value is 0
|
||||
)
|
||||
|
||||
|
||||
class ThreadLocalFileContext(FileLockContext, local):
|
||||
@ -145,7 +147,10 @@ class FileLockMeta(ABCMeta):
|
||||
# parameters do not match; raise error
|
||||
msg = "Singleton lock instances cannot be initialized with differing arguments"
|
||||
msg += "\nNon-matching arguments: "
|
||||
for param_name, (passed_param, set_param) in non_matching_params.items():
|
||||
for param_name, (
|
||||
passed_param,
|
||||
set_param,
|
||||
) in non_matching_params.items():
|
||||
msg += f"\n\t{param_name} (existing lock has {set_param} but {passed_param} was passed)"
|
||||
raise ValueError(msg)
|
||||
|
||||
@ -165,7 +170,9 @@ class FileLockMeta(ABCMeta):
|
||||
}
|
||||
|
||||
present_params = inspect.signature(cls.__init__).parameters
|
||||
init_params = {key: value for key, value in all_params.items() if key in present_params}
|
||||
init_params = {
|
||||
key: value for key, value in all_params.items() if key in present_params
|
||||
}
|
||||
|
||||
instance = super().__call__(lock_file, **init_params)
|
||||
|
||||
@ -239,7 +246,9 @@ class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta):
|
||||
"poll_interval": poll_interval,
|
||||
"lifetime": lifetime,
|
||||
}
|
||||
self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)(**kwargs)
|
||||
self._context: FileLockContext = (
|
||||
ThreadLocalFileContext if thread_local else FileLockContext
|
||||
)(**kwargs)
|
||||
|
||||
def is_thread_local(self) -> bool:
|
||||
""":returns: a flag indicating if this lock is thread local or not"""
|
||||
@ -403,10 +412,14 @@ class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta):
|
||||
start_time: float,
|
||||
) -> bool:
|
||||
if blocking is False:
|
||||
_LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename)
|
||||
_LOGGER.debug(
|
||||
"Failed to immediately acquire lock %s on %s", lock_id, lock_filename
|
||||
)
|
||||
return True
|
||||
if cancel_check is not None and cancel_check():
|
||||
_LOGGER.debug("Cancellation requested for lock %s on %s", lock_id, lock_filename)
|
||||
_LOGGER.debug(
|
||||
"Cancellation requested for lock %s on %s", lock_id, lock_filename
|
||||
)
|
||||
return True
|
||||
if 0 <= timeout < time.perf_counter() - start_time:
|
||||
_LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename)
|
||||
@ -470,7 +483,9 @@ class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta):
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
poll_interval = poll_intervall
|
||||
|
||||
poll_interval = poll_interval if poll_interval is not None else self._context.poll_interval
|
||||
poll_interval = (
|
||||
poll_interval if poll_interval is not None else self._context.poll_interval
|
||||
)
|
||||
|
||||
# Increment the number right at the beginning. We can still undo it, if something fails.
|
||||
self._context.lock_counter += 1
|
||||
@ -479,8 +494,17 @@ class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta):
|
||||
lock_filename = self.lock_file
|
||||
canonical = _canonical(lock_filename)
|
||||
|
||||
would_block = self._context.lock_counter == 1 and not self.is_locked and timeout < 0 and blocking
|
||||
if would_block and (existing := _registry.held.get(canonical)) is not None and existing != lock_id:
|
||||
would_block = (
|
||||
self._context.lock_counter == 1
|
||||
and not self.is_locked
|
||||
and timeout < 0
|
||||
and blocking
|
||||
)
|
||||
if (
|
||||
would_block
|
||||
and (existing := _registry.held.get(canonical)) is not None
|
||||
and existing != lock_id
|
||||
):
|
||||
self._context.lock_counter -= 1
|
||||
msg = (
|
||||
f"Deadlock: lock '{lock_filename}' is already held by a different "
|
||||
@ -494,7 +518,9 @@ class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta):
|
||||
while True:
|
||||
if not self.is_locked:
|
||||
self._try_break_expired_lock()
|
||||
_LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
|
||||
_LOGGER.debug(
|
||||
"Attempting to acquire lock %s on %s", lock_id, lock_filename
|
||||
)
|
||||
self._acquire()
|
||||
if self.is_locked:
|
||||
_LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
|
||||
@ -534,7 +560,9 @@ class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta):
|
||||
if self._context.lock_counter == 0 or force:
|
||||
lock_id, lock_filename = id(self), self.lock_file
|
||||
|
||||
_LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
|
||||
_LOGGER.debug(
|
||||
"Attempting to release lock %s on %s", lock_id, lock_filename
|
||||
)
|
||||
self._release()
|
||||
self._context.lock_counter = 0
|
||||
_registry.held.pop(_canonical(lock_filename), None)
|
||||
|
||||
@ -51,7 +51,11 @@ def timeout_for_sqlite(timeout: float, *, blocking: bool, already_waited: float)
|
||||
remaining = max(timeout - already_waited, 0) if timeout > 0 else timeout
|
||||
timeout_ms = int(remaining * 1000)
|
||||
if timeout_ms > _MAX_SQLITE_TIMEOUT_MS or timeout_ms < 0:
|
||||
_LOGGER.warning("timeout %s is too large for SQLite, using %s ms instead", timeout, _MAX_SQLITE_TIMEOUT_MS)
|
||||
_LOGGER.warning(
|
||||
"timeout %s is too large for SQLite, using %s ms instead",
|
||||
timeout,
|
||||
_MAX_SQLITE_TIMEOUT_MS,
|
||||
)
|
||||
return _MAX_SQLITE_TIMEOUT_MS
|
||||
return timeout_ms
|
||||
|
||||
@ -77,12 +81,16 @@ class _ReadWriteLockMeta(type):
|
||||
is_singleton: bool = True,
|
||||
) -> ReadWriteLock:
|
||||
if not is_singleton:
|
||||
return super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton)
|
||||
return super().__call__(
|
||||
lock_file, timeout, blocking=blocking, is_singleton=is_singleton
|
||||
)
|
||||
|
||||
normalized = pathlib.Path(lock_file).resolve()
|
||||
with cls._instances_lock:
|
||||
if normalized not in cls._instances:
|
||||
instance = super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton)
|
||||
instance = super().__call__(
|
||||
lock_file, timeout, blocking=blocking, is_singleton=is_singleton
|
||||
)
|
||||
cls._instances[normalized] = instance
|
||||
else:
|
||||
instance = cls._instances[normalized]
|
||||
@ -122,7 +130,11 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
|
||||
@classmethod
|
||||
def get_lock(
|
||||
cls, lock_file: str | os.PathLike[str], timeout: float = -1, *, blocking: bool = True
|
||||
cls,
|
||||
lock_file: str | os.PathLike[str],
|
||||
timeout: float = -1,
|
||||
*,
|
||||
blocking: bool = True,
|
||||
) -> ReadWriteLock:
|
||||
"""
|
||||
Return the singleton :class:`ReadWriteLock` for *lock_file*.
|
||||
@ -149,8 +161,12 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
self.lock_file = os.fspath(lock_file)
|
||||
self.timeout = timeout
|
||||
self.blocking = blocking
|
||||
self._transaction_lock = threading.Lock() # serializes the (possibly blocking) SQLite transaction work
|
||||
self._internal_lock = threading.Lock() # protects _lock_level / _current_mode updates and rollback
|
||||
self._transaction_lock = (
|
||||
threading.Lock()
|
||||
) # serializes the (possibly blocking) SQLite transaction work
|
||||
self._internal_lock = (
|
||||
threading.Lock()
|
||||
) # protects _lock_level / _current_mode updates and rollback
|
||||
self._lock_level = 0
|
||||
self._current_mode: Literal["read", "write"] | None = None
|
||||
self._write_thread_id: int | None = None
|
||||
@ -167,7 +183,9 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
if not acquired:
|
||||
raise Timeout(self.lock_file) from None
|
||||
|
||||
def _validate_reentrant(self, mode: Literal["read", "write"], opposite: str, direction: str) -> AcquireReturnProxy:
|
||||
def _validate_reentrant(
|
||||
self, mode: Literal["read", "write"], opposite: str, direction: str
|
||||
) -> AcquireReturnProxy:
|
||||
if self._current_mode != mode:
|
||||
msg = (
|
||||
f"Cannot acquire {mode} lock on {self.lock_file} (lock id: {id(self)}): "
|
||||
@ -184,10 +202,17 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
return AcquireReturnProxy(lock=self)
|
||||
|
||||
def _configure_and_begin(
|
||||
self, mode: Literal["read", "write"], timeout: float, *, blocking: bool, start_time: float
|
||||
self,
|
||||
mode: Literal["read", "write"],
|
||||
timeout: float,
|
||||
*,
|
||||
blocking: bool,
|
||||
start_time: float,
|
||||
) -> None:
|
||||
waited = time.perf_counter() - start_time
|
||||
timeout_ms = timeout_for_sqlite(timeout, blocking=blocking, already_waited=waited)
|
||||
timeout_ms = timeout_for_sqlite(
|
||||
timeout, blocking=blocking, already_waited=waited
|
||||
)
|
||||
self._con.execute(f"PRAGMA busy_timeout={timeout_ms};").close()
|
||||
# Use legacy journal mode (not WAL) because WAL does not block readers when a concurrent EXCLUSIVE
|
||||
# write transaction is active, making read-write locking impossible without modifying table data.
|
||||
@ -199,16 +224,24 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
self._con.execute("PRAGMA journal_mode=MEMORY;").close()
|
||||
# Recompute remaining timeout after the potentially blocking journal_mode pragma.
|
||||
waited = time.perf_counter() - start_time
|
||||
if (recomputed := timeout_for_sqlite(timeout, blocking=blocking, already_waited=waited)) != timeout_ms:
|
||||
if (
|
||||
recomputed := timeout_for_sqlite(
|
||||
timeout, blocking=blocking, already_waited=waited
|
||||
)
|
||||
) != timeout_ms:
|
||||
self._con.execute(f"PRAGMA busy_timeout={recomputed};").close()
|
||||
stmt = "BEGIN EXCLUSIVE TRANSACTION;" if mode == "write" else "BEGIN TRANSACTION;"
|
||||
stmt = (
|
||||
"BEGIN EXCLUSIVE TRANSACTION;" if mode == "write" else "BEGIN TRANSACTION;"
|
||||
)
|
||||
self._con.execute(stmt).close()
|
||||
if mode == "read":
|
||||
# A SELECT is needed to force SQLite to actually acquire the SHARED lock on the database.
|
||||
# https://www.sqlite.org/lockingv3.html#transaction_control
|
||||
self._con.execute("SELECT name FROM sqlite_schema LIMIT 1;").close()
|
||||
|
||||
def _acquire(self, mode: Literal["read", "write"], timeout: float, *, blocking: bool) -> AcquireReturnProxy:
|
||||
def _acquire(
|
||||
self, mode: Literal["read", "write"], timeout: float, *, blocking: bool
|
||||
) -> AcquireReturnProxy:
|
||||
opposite = "write" if mode == "read" else "read"
|
||||
direction = "downgrade" if mode == "read" else "upgrade"
|
||||
|
||||
@ -224,7 +257,9 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
if self._lock_level > 0:
|
||||
return self._validate_reentrant(mode, opposite, direction)
|
||||
|
||||
self._configure_and_begin(mode, timeout, blocking=blocking, start_time=start_time)
|
||||
self._configure_and_begin(
|
||||
mode, timeout, blocking=blocking, start_time=start_time
|
||||
)
|
||||
|
||||
with self._internal_lock:
|
||||
self._current_mode = mode
|
||||
@ -241,7 +276,9 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
finally:
|
||||
self._transaction_lock.release()
|
||||
|
||||
def acquire_read(self, timeout: float = -1, *, blocking: bool = True) -> AcquireReturnProxy:
|
||||
def acquire_read(
|
||||
self, timeout: float = -1, *, blocking: bool = True
|
||||
) -> AcquireReturnProxy:
|
||||
"""
|
||||
Acquire a shared read lock.
|
||||
|
||||
@ -259,7 +296,9 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
"""
|
||||
return self._acquire("read", timeout, blocking=blocking)
|
||||
|
||||
def acquire_write(self, timeout: float = -1, *, blocking: bool = True) -> AcquireReturnProxy:
|
||||
def acquire_write(
|
||||
self, timeout: float = -1, *, blocking: bool = True
|
||||
) -> AcquireReturnProxy:
|
||||
"""
|
||||
Acquire an exclusive write lock.
|
||||
|
||||
@ -309,7 +348,9 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
self._con.rollback()
|
||||
|
||||
@contextmanager
|
||||
def read_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]:
|
||||
def read_lock(
|
||||
self, timeout: float | None = None, *, blocking: bool | None = None
|
||||
) -> Generator[None]:
|
||||
"""
|
||||
Context manager that acquires and releases a shared read lock.
|
||||
|
||||
@ -330,7 +371,9 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
|
||||
self.release()
|
||||
|
||||
@contextmanager
|
||||
def write_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]:
|
||||
def write_lock(
|
||||
self, timeout: float | None = None, *, blocking: bool | None = None
|
||||
) -> Generator[None]:
|
||||
"""
|
||||
Context manager that acquires and releases an exclusive write lock.
|
||||
|
||||
|
||||
@ -44,10 +44,13 @@ class SoftFileLock(BaseFileLock):
|
||||
file_handler = os.open(self.lock_file, flags, self._open_mode())
|
||||
except OSError as exception:
|
||||
if not (
|
||||
exception.errno == EEXIST or (exception.errno == EACCES and sys.platform == "win32")
|
||||
exception.errno == EEXIST
|
||||
or (exception.errno == EACCES and sys.platform == "win32")
|
||||
): # pragma: win32 no cover
|
||||
raise
|
||||
if exception.errno == EEXIST and sys.platform != "win32": # pragma: win32 no cover
|
||||
if (
|
||||
exception.errno == EEXIST and sys.platform != "win32"
|
||||
): # pragma: win32 no cover
|
||||
self._try_break_stale_lock()
|
||||
else:
|
||||
self._write_lock_info(file_handler)
|
||||
|
||||
@ -89,10 +89,17 @@ else: # pragma: win32 no cover
|
||||
def _fallback_to_soft_lock(self) -> None:
|
||||
from ._soft import SoftFileLock # noqa: PLC0415
|
||||
|
||||
warnings.warn("flock not supported on this filesystem, falling back to SoftFileLock", stacklevel=2)
|
||||
warnings.warn(
|
||||
"flock not supported on this filesystem, falling back to SoftFileLock",
|
||||
stacklevel=2,
|
||||
)
|
||||
from .asyncio import AsyncSoftFileLock, BaseAsyncFileLock # noqa: PLC0415
|
||||
|
||||
self.__class__ = AsyncSoftFileLock if isinstance(self, BaseAsyncFileLock) else SoftFileLock
|
||||
self.__class__ = (
|
||||
AsyncSoftFileLock
|
||||
if isinstance(self, BaseAsyncFileLock)
|
||||
else SoftFileLock
|
||||
)
|
||||
|
||||
def _release(self) -> None:
|
||||
fd = cast("int", self._context.lock_file_fd)
|
||||
|
||||
@ -24,7 +24,9 @@ def raise_on_not_writable_file(filename: str) -> None:
|
||||
except OSError:
|
||||
return # swallow does not exist or other errors
|
||||
|
||||
if file_stat.st_mtime != 0: # if os.stat returns but modification is zero that's an invalid os.stat - ignore it
|
||||
if (
|
||||
file_stat.st_mtime != 0
|
||||
): # if os.stat returns but modification is zero that's an invalid os.stat - ignore it
|
||||
if not (file_stat.st_mode & stat.S_IWUSR):
|
||||
raise PermissionError(EACCES, "Permission denied", filename)
|
||||
|
||||
|
||||
@ -55,7 +55,9 @@ if sys.platform == "win32": # pragma: win32 cover
|
||||
# Security check: Refuse to open reparse points (symlinks, junctions)
|
||||
# This prevents TOCTOU symlink attacks (CVE-TBD)
|
||||
if _is_reparse_point(self.lock_file):
|
||||
msg = f"Lock file is a reparse point (symlink/junction): {self.lock_file}"
|
||||
msg = (
|
||||
f"Lock file is a reparse point (symlink/junction): {self.lock_file}"
|
||||
)
|
||||
raise OSError(msg)
|
||||
|
||||
flags = (
|
||||
|
||||
@ -168,9 +168,9 @@ class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta):
|
||||
"run_in_executor": run_in_executor,
|
||||
"executor": executor,
|
||||
}
|
||||
self._context: AsyncFileLockContext = (AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext)(
|
||||
**kwargs
|
||||
)
|
||||
self._context: AsyncFileLockContext = (
|
||||
AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext
|
||||
)(**kwargs)
|
||||
|
||||
@property
|
||||
def run_in_executor(self) -> bool:
|
||||
@ -256,7 +256,9 @@ class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta):
|
||||
while True:
|
||||
if not self.is_locked:
|
||||
self._try_break_expired_lock()
|
||||
_LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
|
||||
_LOGGER.debug(
|
||||
"Attempting to acquire lock %s on %s", lock_id, lock_filename
|
||||
)
|
||||
await self._run_internal_method(self._acquire)
|
||||
if self.is_locked:
|
||||
_LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
|
||||
@ -278,7 +280,9 @@ class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta):
|
||||
raise
|
||||
return AsyncAcquireReturnProxy(lock=self)
|
||||
|
||||
async def release(self, force: bool = False) -> None: # ty: ignore[invalid-method-override] # noqa: FBT001, FBT002
|
||||
async def release(
|
||||
self, force: bool = False
|
||||
) -> None: # ty: ignore[invalid-method-override] # noqa: FBT001, FBT002
|
||||
"""
|
||||
Release the file lock. The lock is only completely released when the lock counter reaches 0. The lock file
|
||||
itself is not automatically deleted.
|
||||
@ -292,7 +296,9 @@ class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta):
|
||||
if self._context.lock_counter == 0 or force:
|
||||
lock_id, lock_filename = id(self), self.lock_file
|
||||
|
||||
_LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
|
||||
_LOGGER.debug(
|
||||
"Attempting to release lock %s on %s", lock_id, lock_filename
|
||||
)
|
||||
await self._run_internal_method(self._release)
|
||||
self._context.lock_counter = 0
|
||||
_LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)
|
||||
|
||||
@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
||||
commit_id: COMMIT_ID
|
||||
__commit_id__: COMMIT_ID
|
||||
|
||||
__version__ = version = '3.24.3'
|
||||
__version__ = version = "3.24.3"
|
||||
__version_tuple__ = version_tuple = (3, 24, 3)
|
||||
|
||||
__commit_id__ = commit_id = None
|
||||
|
||||
@ -3,4 +3,3 @@ Generator: setuptools (75.5.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user