From 740c3c086566cbf34054deda6c29bd2bc36df1cf Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 18 Apr 2023 09:34:41 -0400 Subject: [PATCH] Vendor in latest packages available (#5657) * Update to the latest vendoring package versions available. * add news fragment * fix vendoring CI --- .github/workflows/ci.yaml | 2 +- news/5657.vendor.rst | 1 + pipenv/vendor/attr/__init__.py | 95 ++- pipenv/vendor/attr/__init__.pyi | 111 +++- pipenv/vendor/attr/_cmp.py | 38 +- pipenv/vendor/attr/_cmp.pyi | 14 +- pipenv/vendor/attr/_compat.py | 58 +- pipenv/vendor/attr/_funcs.py | 129 ++-- pipenv/vendor/attr/_make.py | 641 +++++++++---------- pipenv/vendor/attr/_next_gen.py | 18 +- pipenv/vendor/attr/_typing_compat.pyi | 15 + pipenv/vendor/attr/converters.py | 2 +- pipenv/vendor/attr/converters.pyi | 2 +- pipenv/vendor/attr/exceptions.py | 15 +- pipenv/vendor/attr/filters.py | 27 +- pipenv/vendor/attr/filters.pyi | 4 +- pipenv/vendor/attr/setters.pyi | 2 +- pipenv/vendor/attr/validators.py | 158 ++++- pipenv/vendor/attr/validators.pyi | 10 +- pipenv/vendor/attrs/LICENSE | 21 - pipenv/vendor/attrs/__init__.py | 15 +- pipenv/vendor/attrs/__init__.pyi | 5 +- pipenv/vendor/click/__init__.py | 4 +- pipenv/vendor/click/_compat.py | 7 +- pipenv/vendor/click/_termui_impl.py | 1 - pipenv/vendor/click/_unicodefun.py | 100 --- pipenv/vendor/click/core.py | 269 ++++---- pipenv/vendor/click/decorators.py | 135 ++-- pipenv/vendor/click/globals.py | 5 +- pipenv/vendor/click/shell_completion.py | 21 +- pipenv/vendor/click/termui.py | 30 +- pipenv/vendor/click/testing.py | 8 +- pipenv/vendor/click/types.py | 71 ++- pipenv/vendor/click/utils.py | 77 +-- pipenv/vendor/click_didyoumean/__init__.py | 44 +- pipenv/vendor/markupsafe/__init__.py | 25 +- pipenv/vendor/markupsafe/_native.py | 12 - pipenv/vendor/pipdeptree/LICENSE | 2 +- pipenv/vendor/pipdeptree/__init__.py | 131 +++- pipenv/vendor/pipdeptree/version.py | 5 +- pipenv/vendor/shellingham/__init__.py | 2 +- pipenv/vendor/tomlkit/__init__.py | 53 +- pipenv/vendor/tomlkit/_compat.py | 5 +- pipenv/vendor/tomlkit/_utils.py | 44 +- pipenv/vendor/tomlkit/api.py | 79 ++- pipenv/vendor/tomlkit/container.py | 160 +++-- pipenv/vendor/tomlkit/exceptions.py | 16 +- pipenv/vendor/tomlkit/items.py | 694 +++++++++++++++------ pipenv/vendor/tomlkit/parser.py | 139 +++-- pipenv/vendor/tomlkit/source.py | 12 +- pipenv/vendor/tomlkit/toml_char.py | 10 +- pipenv/vendor/tomlkit/toml_document.py | 2 +- pipenv/vendor/tomlkit/toml_file.py | 45 +- pipenv/vendor/vendor.txt | 14 +- 54 files changed, 2254 insertions(+), 1351 deletions(-) create mode 100644 news/5657.vendor.rst create mode 100644 pipenv/vendor/attr/_typing_compat.pyi delete mode 100644 pipenv/vendor/attrs/LICENSE delete mode 100644 pipenv/vendor/click/_unicodefun.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2b588f299..ca996bc170 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -76,7 +76,7 @@ jobs: with: api_key: ${{ secrets.FORESIGHT_API_KEY }} - run: | - python -m pip install --upgrade wheel invoke parver beautifulsoup4 vistir towncrier requests parse + python -m pip install --upgrade wheel invoke parver beautifulsoup4 vistir towncrier requests parse hatch-fancy-pypi-readme python -m invoke vendoring.update tests: name: ${{matrix.os}} / ${{ matrix.python-version }} diff --git a/news/5657.vendor.rst b/news/5657.vendor.rst new file mode 100644 index 0000000000..7079494c87 --- /dev/null +++ b/news/5657.vendor.rst @@ -0,0 +1 @@ +Vendor in latest available dependencies: ``attrs==23.1.0`` ``click-didyoumean==0.3.0`` ``click==8.1.3`` ``markupsafe==2.1.2`` ``pipdeptree==2.7.0`` ``shellingham==1.5.0.post1`` ``tomlkit==0.11.7`` diff --git a/pipenv/vendor/attr/__init__.py b/pipenv/vendor/attr/__init__.py index 386305d628..7cfa792f74 100644 --- a/pipenv/vendor/attr/__init__.py +++ b/pipenv/vendor/attr/__init__.py @@ -1,9 +1,11 @@ # SPDX-License-Identifier: MIT - -import sys +""" +Classes Without Boilerplate +""" from functools import partial +from typing import Callable from . import converters, exceptions, filters, setters, validators from ._cmp import cmp_using @@ -20,31 +22,22 @@ make_class, validate, ) +from ._next_gen import define, field, frozen, mutable from ._version_info import VersionInfo -__version__ = "22.1.0" -__version_info__ = VersionInfo._from_version_string(__version__) - -__title__ = "attrs" -__description__ = "Classes Without Boilerplate" -__url__ = "https://www.attrs.org/" -__uri__ = __url__ -__doc__ = __description__ + " <" + __uri__ + ">" - -__author__ = "Hynek Schlawack" -__email__ = "hs@ox.cx" - -__license__ = "MIT" -__copyright__ = "Copyright (c) 2015 Hynek Schlawack" - - s = attributes = attrs ib = attr = attrib dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +class AttrsInstance: + pass + + __all__ = [ "Attribute", + "AttrsInstance", "Factory", "NOTHING", "asdict", @@ -56,15 +49,19 @@ "attrs", "cmp_using", "converters", + "define", "evolve", "exceptions", + "field", "fields", "fields_dict", "filters", + "frozen", "get_run_validators", "has", "ib", "make_class", + "mutable", "resolve_types", "s", "set_run_validators", @@ -73,7 +70,63 @@ "validators", ] -if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable # noqa: F401 - __all__.extend(("define", "field", "frozen", "mutable")) +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + dunder_to_metadata = { + "__title__": "Name", + "__copyright__": "", + "__version__": "version", + "__version_info__": "version", + "__description__": "summary", + "__uri__": "", + "__url__": "", + "__author__": "", + "__email__": "", + "__license__": "license", + } + if name not in dunder_to_metadata.keys(): + raise AttributeError(f"module {mod_name} has no attribute {name}") + + import sys + import warnings + + if sys.version_info < (3, 8): + from importlib_metadata import metadata + else: + from importlib.metadata import metadata + + if name != "__version_info__": + warnings.warn( + f"Accessing {mod_name}.{name} is deprecated and will be " + "removed in a future release. Use importlib.metadata directly " + "to query for attrs's packaging metadata.", + DeprecationWarning, + stacklevel=2, + ) + + meta = metadata("attrs") + if name == "__license__": + return "MIT" + elif name == "__copyright__": + return "Copyright (c) 2015 Hynek Schlawack" + elif name in ("__uri__", "__url__"): + return meta["Project-URL"].split(" ", 1)[-1] + elif name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + elif name == "__author__": + return meta["Author-email"].rsplit(" ", 1)[0] + elif name == "__email__": + return meta["Author-email"].rsplit("<", 1)[1][:-1] + + return meta[dunder_to_metadata[name]] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/pipenv/vendor/attr/__init__.pyi b/pipenv/vendor/attr/__init__.pyi index 03cc4c82d2..ced5a3fd40 100644 --- a/pipenv/vendor/attr/__init__.pyi +++ b/pipenv/vendor/attr/__init__.pyi @@ -1,9 +1,9 @@ +import enum import sys from typing import ( Any, Callable, - ClassVar, Dict, Generic, List, @@ -25,8 +25,14 @@ from . import filters as filters from . import setters as setters from . import validators as validators from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ from ._version_info import VersionInfo +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard + __version__: str __version_info__: VersionInfo __title__: str @@ -42,30 +48,34 @@ _T = TypeVar("_T") _C = TypeVar("_C", bound=type) _EqOrderType = Union[bool, Callable[[Any], Any]] -_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] +_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] _ConverterType = Callable[[Any], Any] -_FilterType = Callable[[Attribute[_T], _T], bool] +_FilterType = Callable[["Attribute[_T]", _T], bool] _ReprType = Callable[[Any], str] _ReprArgType = Union[bool, _ReprType] -_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] +_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] _OnSetAttrArgType = Union[ _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType ] _FieldTransformer = Callable[ - [type, List[Attribute[Any]]], List[Attribute[Any]] + [type, List["Attribute[Any]"]], List["Attribute[Any]"] ] # FIXME: in reality, if multiple validators are passed they must be in a list # or tuple, but those are invariant and so would prevent subtypes of # _ValidatorType from working when passed in a list or tuple. _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] -# A protocol to be able to statically accept an attrs class. -class AttrsInstance(Protocol): - __attrs_attrs__: ClassVar[Any] +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass +_A = TypeVar("_A", bound=AttrsInstance) # _make -- -NOTHING: object +class _Nothing(enum.Enum): + NOTHING = enum.auto() + +NOTHING = _Nothing.NOTHING # NOTE: Factory lies about its return type to make this possible: # `x: List[int] # = Factory(list)` @@ -107,6 +117,7 @@ def __dataclass_transform__( eq_default: bool = True, order_default: bool = False, kw_only_default: bool = False, + frozen_default: bool = False, field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), ) -> Callable[[_T], _T]: ... @@ -125,6 +136,8 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] kw_only: bool on_setattr: _OnSetAttrType + alias: Optional[str] + def evolve(self, **changes: Any) -> "Attribute[Any]": ... # NOTE: We had several choices for the annotation to use for type arg: @@ -167,6 +180,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -187,6 +201,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> _T: ... # This form catches an explicit default argument. @@ -206,6 +221,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @@ -225,6 +241,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> Any: ... @overload def field( @@ -241,6 +258,8 @@ def field( eq: Optional[bool] = ..., order: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -260,6 +279,8 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form catches an explicit default argument. @@ -278,6 +299,8 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @@ -296,6 +319,8 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... @overload @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) @@ -323,6 +348,7 @@ def attrs( on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., + unsafe_hash: Optional[bool] = ..., ) -> _C: ... @overload @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) @@ -350,6 +376,7 @@ def attrs( on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., + unsafe_hash: Optional[bool] = ..., ) -> Callable[[_C], _C]: ... @overload @__dataclass_transform__(field_descriptors=(attrib, field)) @@ -358,6 +385,7 @@ def define( *, these: Optional[Dict[str, Any]] = ..., repr: bool = ..., + unsafe_hash: Optional[bool] = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., @@ -383,6 +411,7 @@ def define( *, these: Optional[Dict[str, Any]] = ..., repr: bool = ..., + unsafe_hash: Optional[bool] = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., @@ -403,17 +432,73 @@ def define( ) -> Callable[[_C], _C]: ... mutable = define -frozen = define # they differ only in their defaults +@overload +@__dataclass_transform__( + frozen_default=True, field_descriptors=(attrib, field) +) +def frozen( + maybe_cls: _C, + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@__dataclass_transform__( + frozen_default=True, field_descriptors=(attrib, field) +) +def frozen( + maybe_cls: None = ..., + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... def fields(cls: Type[AttrsInstance]) -> Any: ... def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ... def validate(inst: AttrsInstance) -> None: ... def resolve_types( - cls: _C, + cls: _A, globalns: Optional[Dict[str, Any]] = ..., localns: Optional[Dict[str, Any]] = ..., attribs: Optional[List[Attribute[Any]]] = ..., -) -> _C: ... + include_extras: bool = ..., +) -> _A: ... # TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', @@ -470,7 +555,7 @@ def astuple( tuple_factory: Type[Sequence[Any]] = ..., retain_collection_types: bool = ..., ) -> Tuple[Any, ...]: ... -def has(cls: type) -> bool: ... +def has(cls: type) -> TypeGuard[Type[AttrsInstance]]: ... def assoc(inst: _T, **changes: Any) -> _T: ... def evolve(inst: _T, **changes: Any) -> _T: ... diff --git a/pipenv/vendor/attr/_cmp.py b/pipenv/vendor/attr/_cmp.py index 81b99e4c33..d9cbe22cde 100644 --- a/pipenv/vendor/attr/_cmp.py +++ b/pipenv/vendor/attr/_cmp.py @@ -20,22 +20,22 @@ def cmp_using( class_name="Comparable", ): """ - Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and - ``cmp`` arguments to customize field comparison. - - The resulting class will have a full set of ordering methods if - at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. - - :param Optional[callable] eq: `callable` used to evaluate equality - of two objects. - :param Optional[callable] lt: `callable` used to evaluate whether - one object is less than another object. - :param Optional[callable] le: `callable` used to evaluate whether - one object is less than or equal to another object. - :param Optional[callable] gt: `callable` used to evaluate whether - one object is greater than another object. - :param Optional[callable] ge: `callable` used to evaluate whether - one object is greater than or equal to another object. + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + :param Optional[callable] eq: `callable` used to evaluate equality of two + objects. + :param Optional[callable] lt: `callable` used to evaluate whether one + object is less than another object. + :param Optional[callable] le: `callable` used to evaluate whether one + object is less than or equal to another object. + :param Optional[callable] gt: `callable` used to evaluate whether one + object is greater than another object. + :param Optional[callable] ge: `callable` used to evaluate whether one + object is greater than or equal to another object. :param bool require_same_type: When `True`, equality and ordering methods will return `NotImplemented` if objects are not of the same type. @@ -130,9 +130,9 @@ def method(self, other): return result - method.__name__ = "__%s__" % (name,) - method.__doc__ = "Return a %s b. Computed by attrs." % ( - _operation_names[name], + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." ) return method diff --git a/pipenv/vendor/attr/_cmp.pyi b/pipenv/vendor/attr/_cmp.pyi index 35437eff62..f3dcdc1a75 100644 --- a/pipenv/vendor/attr/_cmp.pyi +++ b/pipenv/vendor/attr/_cmp.pyi @@ -3,11 +3,11 @@ from typing import Any, Callable, Optional, Type _CompareWithType = Callable[[Any, Any], bool] def cmp_using( - eq: Optional[_CompareWithType], - lt: Optional[_CompareWithType], - le: Optional[_CompareWithType], - gt: Optional[_CompareWithType], - ge: Optional[_CompareWithType], - require_same_type: bool, - class_name: str, + eq: Optional[_CompareWithType] = ..., + lt: Optional[_CompareWithType] = ..., + le: Optional[_CompareWithType] = ..., + gt: Optional[_CompareWithType] = ..., + ge: Optional[_CompareWithType] = ..., + require_same_type: bool = ..., + class_name: str = ..., ) -> Type: ... diff --git a/pipenv/vendor/attr/_compat.py b/pipenv/vendor/attr/_compat.py index 5826493257..c3bf5e33ba 100644 --- a/pipenv/vendor/attr/_compat.py +++ b/pipenv/vendor/attr/_compat.py @@ -9,20 +9,13 @@ import warnings from collections.abc import Mapping, Sequence # noqa +from typing import _GenericAlias PYPY = platform.python_implementation() == "PyPy" -PY36 = sys.version_info[:2] >= (3, 6) -HAS_F_STRINGS = PY36 +PY_3_9_PLUS = sys.version_info[:2] >= (3, 9) PY310 = sys.version_info[:2] >= (3, 10) - - -if PYPY or PY36: - ordered_dict = dict -else: - from collections import OrderedDict - - ordered_dict = OrderedDict +PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) def just_warn(*args, **kw): @@ -90,32 +83,32 @@ def set_closure_cell(cell, value): # Otherwise gotta do it the hard way. - # Create a function that will set its first cellvar to `value`. - def set_first_cellvar_to(value): - x = value - return - - # This function will be eliminated as dead code, but - # not before its reference to `x` forces `x` to be - # represented as a closure cell rather than a local. - def force_x_to_be_a_cell(): # pragma: no cover - return x - try: - # Extract the code object and make sure our assumptions about - # the closure behavior are correct. - co = set_first_cellvar_to.__code__ - if co.co_cellvars != ("x",) or co.co_freevars != (): - raise AssertionError # pragma: no cover - - # Convert this code object to a code object that sets the - # function's first _freevar_ (not cellvar) to the argument. if sys.version_info >= (3, 8): def set_closure_cell(cell, value): cell.cell_contents = value else: + # Create a function that will set its first cellvar to `value`. + def set_first_cellvar_to(value): + x = value + return + + # This function will be eliminated as dead code, but + # not before its reference to `x` forces `x` to be + # represented as a closure cell rather than a local. + def force_x_to_be_a_cell(): # pragma: no cover + return x + + # Extract the code object and make sure our assumptions about + # the closure behavior are correct. + co = set_first_cellvar_to.__code__ + if co.co_cellvars != ("x",) or co.co_freevars != (): + raise AssertionError # pragma: no cover + + # Convert this code object to a code object that sets the + # function's first _freevar_ (not cellvar) to the argument. args = [co.co_argcount] args.append(co.co_kwonlyargcount) args.extend( @@ -183,3 +176,10 @@ def func(): # don't have a direct reference to the thread-local in their globals dict. # If they have such a reference, it breaks cloudpickle. repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/pipenv/vendor/attr/_funcs.py b/pipenv/vendor/attr/_funcs.py index a982d7cb56..7f5d9610f3 100644 --- a/pipenv/vendor/attr/_funcs.py +++ b/pipenv/vendor/attr/_funcs.py @@ -3,6 +3,7 @@ import copy +from ._compat import PY_3_9_PLUS, get_generic_base from ._make import NOTHING, _obj_setattr, fields from .exceptions import AttrsAttributeNotFoundError @@ -16,13 +17,13 @@ def asdict( value_serializer=None, ): """ - Return the ``attrs`` attribute values of *inst* as a dict. + Return the *attrs* attribute values of *inst* as a dict. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -40,7 +41,7 @@ def asdict( :rtype: return type of *dict_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.0.0 *dict_factory* @@ -195,13 +196,13 @@ def astuple( retain_collection_types=False, ): """ - Return the ``attrs`` attribute values of *inst* as a tuple. + Return the *attrs* attribute values of *inst* as a tuple. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -215,7 +216,7 @@ def astuple( :rtype: return type of *tuple_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.2.0 @@ -289,28 +290,48 @@ def astuple( def has(cls): """ - Check whether *cls* is a class with ``attrs`` attributes. + Check whether *cls* is a class with *attrs* attributes. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. :rtype: bool """ - return getattr(cls, "__attrs_attrs__", None) is not None + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False def assoc(inst, **changes): """ Copy *inst* and apply *changes*. - :param inst: Instance of a class with ``attrs`` attributes. + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. - :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't - be found on *cls*. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.AttrsAttributeNotFoundError: If *attr_name* + couldn't be found on *cls*. + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. deprecated:: 17.1.0 @@ -318,57 +339,83 @@ def assoc(inst, **changes): This function will not be removed du to the slightly different approach compared to `attrs.evolve`. """ - import warnings - - warnings.warn( - "assoc is deprecated and will be removed after 2018/01.", - DeprecationWarning, - stacklevel=2, - ) new = copy.copy(inst) attrs = fields(inst.__class__) for k, v in changes.items(): a = getattr(attrs, k, NOTHING) if a is NOTHING: raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}.".format( - k=k, cl=new.__class__ - ) + f"{k} is not an attrs attribute on {new.__class__}." ) _obj_setattr(new, k, v) return new -def evolve(inst, **changes): +def evolve(*args, **changes): """ - Create a new instance, based on *inst* with *changes* applied. + Create a new instance, based on the first positional argument with + *changes* applied. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. :raise TypeError: If *attr_name* couldn't be found in the class ``__init__``. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. - .. versionadded:: 17.1.0 + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. """ + # Try to get instance by positional argument first. + # Use changes otherwise and warn it'll break. + if args: + try: + (inst,) = args + except ValueError: + raise TypeError( + f"evolve() takes 1 positional argument, but {len(args)} " + "were given" + ) from None + else: + try: + inst = changes.pop("inst") + except KeyError: + raise TypeError( + "evolve() missing 1 required positional argument: 'inst'" + ) from None + + import warnings + + warnings.warn( + "Passing the instance per keyword argument is deprecated and " + "will stop working in, or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) + cls = inst.__class__ attrs = fields(cls) for a in attrs: if not a.init: continue attr_name = a.name # To deal with private attributes. - init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + init_name = a.alias if init_name not in changes: changes[init_name] = getattr(inst, attr_name) return cls(**changes) -def resolve_types(cls, globalns=None, localns=None, attribs=None): +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): """ Resolve any strings and forward annotations in type annotations. @@ -387,10 +434,14 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): :param Optional[dict] localns: Dictionary containing local variables. :param Optional[list] attribs: List of attribs for the given class. This is necessary when calling from inside a ``field_transformer`` - since *cls* is not an ``attrs`` class yet. + since *cls* is not an *attrs* class yet. + :param bool include_extras: Resolve more accurately, if possible. + Pass ``include_extras`` to ``typing.get_hints``, if supported by the + typing module. On supported Python versions (3.9+), this resolves the + types more accurately. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class and you didn't pass any attribs. :raise NameError: If types cannot be resolved because of missing variables. @@ -400,6 +451,7 @@ class and you didn't pass any attribs. .. versionadded:: 20.1.0 .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* """ # Since calling get_type_hints is expensive we cache whether we've @@ -407,7 +459,12 @@ class and you didn't pass any attribs. if getattr(cls, "__attrs_types_resolved__", None) != cls: import typing - hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + kwargs = {"globalns": globalns, "localns": localns} + + if PY_3_9_PLUS: + kwargs["include_extras"] = include_extras + + hints = typing.get_type_hints(cls, **kwargs) for field in fields(cls) if attribs is None else attribs: if field.name in hints: # Since fields have been frozen we must work around it. diff --git a/pipenv/vendor/attr/_make.py b/pipenv/vendor/attr/_make.py index 4d1afe3fc8..d72f738eec 100644 --- a/pipenv/vendor/attr/_make.py +++ b/pipenv/vendor/attr/_make.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MIT import copy +import enum import linecache import sys import types @@ -12,11 +13,9 @@ # having the thread-local in the globals here. from . import _compat, _config, setters from ._compat import ( - HAS_F_STRINGS, PY310, - PYPY, _AnnotationExtractor, - ordered_dict, + get_generic_base, set_closure_cell, ) from .exceptions import ( @@ -30,10 +29,7 @@ # This is used at least twice, so cache it here. _obj_setattr = object.__setattr__ _init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_{}" -_tuple_property_pat = ( - " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" -) +_init_factory_pat = "__attr_factory_%s" _classvar_prefixes = ( "typing.ClassVar", "t.ClassVar", @@ -53,21 +49,18 @@ _ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) -class _Nothing: +class _Nothing(enum.Enum): """ - Sentinel class to indicate the lack of a value when ``None`` is ambiguous. + Sentinel to indicate the lack of a value when ``None`` is ambiguous. - ``_Nothing`` is a singleton. There is only ever one of it. + If extending attrs, you can use ``typing.Literal[NOTHING]`` to show + that a value may be ``NOTHING``. .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. """ - _singleton = None - - def __new__(cls): - if _Nothing._singleton is None: - _Nothing._singleton = super().__new__(cls) - return _Nothing._singleton + NOTHING = enum.auto() def __repr__(self): return "NOTHING" @@ -76,7 +69,7 @@ def __bool__(self): return False -NOTHING = _Nothing() +NOTHING = _Nothing.NOTHING """ Sentinel to indicate the lack of a value when ``None`` is ambiguous. """ @@ -113,6 +106,7 @@ def attrib( eq=None, order=None, on_setattr=None, + alias=None, ): """ Create a new attribute on a class. @@ -120,9 +114,12 @@ def attrib( .. warning:: Does *not* do anything unless the class is also decorated with - `attr.s`! + `attr.s` / `attrs.define` / et cetera! - :param default: A value that is used if an ``attrs``-generated ``__init__`` + Please consider using `attrs.field` in new code (``attr.ib`` will *never* + go away, though). + + :param default: A value that is used if an *attrs*-generated ``__init__`` is used and no value is passed while instantiating or the attribute is excluded using ``init=False``. @@ -141,7 +138,7 @@ def attrib( :param callable factory: Syntactic sugar for ``default=attr.Factory(factory)``. - :param validator: `callable` that is called by ``attrs``-generated + :param validator: `callable` that is called by *attrs*-generated ``__init__`` methods after the instance has been initialized. They receive the initialized instance, the :func:`~attrs.Attribute`, and the passed value. @@ -153,7 +150,7 @@ def attrib( all pass. Validators can be globally disabled and re-enabled using - `get_run_validators`. + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. The validator can also be set using decorator notation as shown below. @@ -195,31 +192,33 @@ def attrib( value. In that case this attributed is unconditionally initialized with the specified default value or factory. :param callable converter: `callable` that is called by - ``attrs``-generated ``__init__`` methods to convert attribute's value + *attrs*-generated ``__init__`` methods to convert attribute's value to the desired format. It is given the passed-in value, and the returned value will be used as the new value of the attribute. The value is converted before being passed to the validator, if any. :param metadata: An arbitrary mapping, to be used by third-party - components. See `extending_metadata`. - :param type: The type of the attribute. In Python 3.6 or greater, the - preferred method to specify the type is using a variable annotation - (see :pep:`526`). + components. See `extending-metadata`. + + :param type: The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). This argument is provided for backward compatibility. Regardless of the approach used, the type will be stored on ``Attribute.type``. - Please note that ``attrs`` doesn't do anything with this metadata by + Please note that *attrs* doesn't do anything with this metadata by itself. You can use it as part of your own code or for `static type checking `. - :param kw_only: Make this attribute keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). + :param kw_only: Make this attribute keyword-only in the generated + ``__init__`` (if ``init`` is ``False``, this parameter is ignored). :param on_setattr: Allows to overwrite the *on_setattr* setting from `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this attribute -- regardless of the setting in `attr.s`. :type on_setattr: `callable`, or a list of callables, or `None`, or `attrs.setters.NO_OP` + :param Optional[str] alias: Override this attribute's parameter name in the + generated ``__init__`` method. If left `None`, default to ``name`` + stripped of leading underscores. See `private-attributes`. .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* @@ -242,6 +241,7 @@ def attrib( .. versionchanged:: 21.1.0 *eq*, *order*, and *cmp* also accept a custom callable .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 22.2.0 *alias* """ eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq, order, True @@ -291,6 +291,7 @@ def attrib( order=order, order_key=order_key, on_setattr=on_setattr, + alias=alias, ) @@ -323,7 +324,7 @@ def _make_method(name, script, filename, globs): if old_val == linecache_tuple: break else: - filename = "{}-{}>".format(base_filename[:-1], count) + filename = f"{base_filename[:-1]}-{count}>" count += 1 _compile_and_eval(script, globs, locs, filename) @@ -341,15 +342,15 @@ class MyClassAttributes(tuple): __slots__ = () x = property(itemgetter(0)) """ - attr_class_name = "{}Attributes".format(cls_name) + attr_class_name = f"{cls_name}Attributes" attr_class_template = [ - "class {}(tuple):".format(attr_class_name), + f"class {attr_class_name}(tuple):", " __slots__ = ()", ] if attr_names: for i, attr_name in enumerate(attr_names): attr_class_template.append( - _tuple_property_pat.format(index=i, attr_name=attr_name) + f" {attr_name} = _attrs_property(_attrs_itemgetter({i}))" ) else: attr_class_template.append(" pass") @@ -393,8 +394,6 @@ def _is_class_var(annot): def _has_own_attribute(cls, attrib_name): """ Check whether *cls* defines *attrib_name* (and doesn't just inherit it). - - Requires Python 3. """ attr = getattr(cls, attrib_name, _sentinel) if attr is _sentinel: @@ -418,13 +417,6 @@ def _get_annotations(cls): return {} -def _counter_getter(e): - """ - Key function for sorting to avoid re-creating a lambda for every class. - """ - return e[1].counter - - def _collect_base_attrs(cls, taken_attr_names): """ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. @@ -502,9 +494,6 @@ def _transform_attrs( if these is not None: ca_list = [(name, ca) for name, ca in these.items()] - - if not isinstance(these, ordered_dict): - ca_list.sort(key=_counter_getter) elif auto_attribs is True: ca_names = { name @@ -576,7 +565,7 @@ def _transform_attrs( if had_default is True and a.default is NOTHING: raise ValueError( "No mandatory attributes allowed after an attribute with a " - "default value or factory. Attribute in question: %r" % (a,) + f"default value or factory. Attribute in question: {a!r}" ) if had_default is False and a.default is not NOTHING: @@ -585,6 +574,14 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) + # Resolve default field alias after executing field_transformer. + # This allows field_transformer to differentiate between explicit vs + # default aliases and supply their own defaults. + attrs = [ + a.evolve(alias=_default_init_alias_for(a.name)) if not a.alias else a + for a in attrs + ] + # Create AttrsClass *after* applying the field_transformer since it may # add or remove attributes! attr_names = [a.name for a in attrs] @@ -593,28 +590,19 @@ def _transform_attrs( return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) -if PYPY: - - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - if isinstance(self, BaseException) and name in ( - "__cause__", - "__context__", - ): - BaseException.__setattr__(self, name, value) - return - - raise FrozenInstanceError() - -else: +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + ): + BaseException.__setattr__(self, name, value) + return - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - raise FrozenInstanceError() + raise FrozenInstanceError() def _frozen_delattrs(self, name): @@ -735,17 +723,35 @@ def __init__( ) = self._make_getstate_setstate() def __repr__(self): - return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + return f"<_ClassBuilder(cls={self._cls.__name__})>" - def build_class(self): - """ - Finalize class based on the accumulated configuration. + if PY310: + import abc + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + + return self.abc.update_abstractmethods( + self._patch_original_class() + ) + + else: + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() - Builder cannot be used after calling this method. - """ - if self._slots is True: - return self._create_slots_class() - else: return self._patch_original_class() def _patch_original_class(self): @@ -862,8 +868,8 @@ def _create_slots_class(self): cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) # The following is a fix for - # . On Python 3, - # if a method mentions `__class__` or uses the no-arg super(), the + # . + # If a method mentions `__class__` or uses the no-arg super(), the # compiler will bake a reference to the class in the method itself # as `method.__closure__`. Since we replace the class with a # clone, we rewrite these references so it keeps working. @@ -924,7 +930,7 @@ def slots_getstate(self): """ Automatically created by attrs. """ - return tuple(getattr(self, name) for name in state_attr_names) + return {name: getattr(self, name) for name in state_attr_names} hash_caching_enabled = self._cache_hash @@ -932,9 +938,16 @@ def slots_setstate(self, state): """ Automatically created by attrs. """ - __bound_setattr = _obj_setattr.__get__(self, Attribute) - for name, value in zip(state_attr_names, state): - __bound_setattr(name, value) + __bound_setattr = _obj_setattr.__get__(self) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) # The hash code cache is not included when the object is # serialized, but it still needs to be initialized to None to @@ -1079,8 +1092,9 @@ def _add_method_dunders(self, method): pass try: - method.__doc__ = "Method generated by attrs for class %s." % ( - self._cls.__qualname__, + method.__doc__ = ( + "Method generated by attrs for class " + f"{self._cls.__qualname__}." ) except AttributeError: pass @@ -1205,24 +1219,24 @@ def attrs( on_setattr=None, field_transformer=None, match_args=True, + unsafe_hash=None, ): r""" - A class decorator that adds `dunder - `_\ -methods according to the + A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. + Please consider using `attrs.define` / `attrs.frozen` in new code + (``attr.s`` will *never* go away, though). + :param these: A dictionary of name to `attr.ib` mappings. This is useful to avoid the definition of your attributes within the class body because you can't (e.g. if you want to add ``__repr__`` methods to Django models) or don't want to. - If *these* is not ``None``, ``attrs`` will *not* search the class body + If *these* is not ``None``, *attrs* will *not* search the class body for attributes and will *not* remove any attributes from it. - If *these* is an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the attributes inside *these*. Otherwise the order - of the definition of the attributes is used. + The order is deduced from the order of the attributes inside *these*. :type these: `dict` of `str` to `attr.ib` @@ -1236,14 +1250,14 @@ def attrs( inherited from some base class). So for example by implementing ``__eq__`` on a class yourself, - ``attrs`` will deduce ``eq=False`` and will create *neither* + *attrs* will deduce ``eq=False`` and will create *neither* ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible ``__ne__`` by default, so it *should* be enough to only implement ``__eq__`` in most cases). .. warning:: - If you prevent ``attrs`` from creating the ordering methods for you + If you prevent *attrs* from creating the ordering methods for you (``order=False``, e.g. by implementing ``__le__``), it becomes *your* responsibility to make sure its ordering is sound. The best way is to use the `functools.total_ordering` decorator. @@ -1252,18 +1266,15 @@ def attrs( Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, *cmp*, or *hash* overrides whatever *auto_detect* would determine. - *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises - an `attrs.exceptions.PythonTooOldError`. - :param bool repr: Create a ``__repr__`` method with a human readable - representation of ``attrs`` attributes.. + representation of *attrs* attributes.. :param bool str: Create a ``__str__`` method that is identical to ``__repr__``. This is usually not necessary except for `Exception`\ s. :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` and ``__ne__`` methods that check two instances for equality. - They compare the instances as if they were tuples of their ``attrs`` + They compare the instances as if they were tuples of their *attrs* attributes if and only if the types of both classes are *identical*! :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` methods that behave like *eq* above and @@ -1271,10 +1282,10 @@ def attrs( *eq*. :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the same value. Must not be mixed with *eq* or *order*. - :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method - is generated according how *eq* and *frozen* are set. + :param Optional[bool] unsafe_hash: If ``None`` (default), the ``__hash__`` + method is generated according how *eq* and *frozen* are set. - 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + 1. If *both* are True, *attrs* will generate a ``__hash__`` for you. 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to None, marking it unhashable (which it is). 3. If *eq* is False, ``__hash__`` will be left untouched meaning the @@ -1282,7 +1293,7 @@ def attrs( ``object``, this means it will fall back to id-based hashing.). Although not recommended, you can decide for yourself and force - ``attrs`` to create one (e.g. if the class is immutable even though you + *attrs* to create one (e.g. if the class is immutable even though you didn't freeze it programmatically) by passing ``True`` or not. Both of these cases are rather special and should be used carefully. @@ -1290,8 +1301,10 @@ def attrs( `object.__hash__`, and the `GitHub issue that led to the default \ behavior `_ for more details. + :param Optional[bool] hash: Alias for *unsafe_hash*. *unsafe_hash* takes + precedence. :param bool init: Create a ``__init__`` method that initializes the - ``attrs`` attributes. Leading underscores are stripped for the argument + *attrs* attributes. Leading underscores are stripped for the argument name. If a ``__attrs_pre_init__`` method exists on the class, it will be called before the class is initialized. If a ``__attrs_post_init__`` method exists on the class, it will be called after the class is fully @@ -1301,13 +1314,13 @@ def attrs( injected instead. This allows you to define a custom ``__init__`` method that can do pre-init work such as ``super().__init__()``, and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. - :param bool slots: Create a `slotted class ` that's more - memory-efficient. Slotted classes are generally superior to the default - dict classes, but have some gotchas you should know about, so we - encourage you to read the `glossary entry `. + :param bool slots: Create a :term:`slotted class ` that's + more memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, so + we encourage you to read the :term:`glossary entry `. :param bool frozen: Make instances immutable after initialization. If someone attempts to modify a frozen instance, - `attr.exceptions.FrozenInstanceError` is raised. + `attrs.exceptions.FrozenInstanceError` is raised. .. note:: @@ -1330,9 +1343,9 @@ def attrs( :param bool weakref_slot: Make instances weak-referenceable. This has no effect unless ``slots`` is also enabled. :param bool auto_attribs: If ``True``, collect :pep:`526`-annotated - attributes (Python 3.6 and later only) from the class body. + attributes from the class body. - In this case, you **must** annotate every field. If ``attrs`` + In this case, you **must** annotate every field. If *attrs* encounters a field that is set to an `attr.ib` but lacks a type annotation, an `attr.exceptions.UnannotatedAttributeError` is raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't @@ -1348,14 +1361,14 @@ def attrs( .. warning:: For features that use the attribute name to create decorators (e.g. - `validators `), you still *must* assign `attr.ib` to - them. Otherwise Python will either not find the name or try to use - the default value to call e.g. ``validator`` on it. + :ref:`validators `), you still *must* assign `attr.ib` + to them. Otherwise Python will either not find the name or try to + use the default value to call e.g. ``validator`` on it. These errors can be quite confusing and probably the most common bug report on our bug tracker. - :param bool kw_only: Make all attributes keyword-only (Python 3+) + :param bool kw_only: Make all attributes keyword-only in the generated ``__init__`` (if ``init`` is ``False``, this parameter is ignored). :param bool cache_hash: Ensure that the object's hash code is computed @@ -1371,14 +1384,14 @@ def attrs( class: - the values for *eq*, *order*, and *hash* are ignored and the - instances compare and hash by the instance's ids (N.B. ``attrs`` will + instances compare and hash by the instance's ids (N.B. *attrs* will *not* remove existing implementations of ``__hash__`` or the equality methods. It just won't add own ones.), - all attributes that are either passed into ``__init__`` or have a default value are additionally available as a tuple in the ``args`` attribute, - the value of *str* is ignored leaving ``__str__`` to base classes. - :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` + :param bool collect_by_mro: Setting this to `True` fixes the way *attrs* collects attributes from base classes. The default behavior is incorrect in certain cases of multiple inheritance. It should be on by default but is kept off for backward-compatibility. @@ -1417,7 +1430,7 @@ def attrs( :param Optional[callable] field_transformer: A function that is called with the original class object and all - fields right before ``attrs`` finalizes the class. You can use + fields right before *attrs* finalizes the class. You can use this, e.g., to automatically add converters or validators to fields based on their types. See `transform-fields` for more details. @@ -1461,9 +1474,14 @@ def attrs( .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionadded:: 21.3.0 *match_args* + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). """ eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) - hash_ = hash # work around the lack of nonlocal + + # unsafe_hash takes precedence due to PEP 681. + if unsafe_hash is not None: + hash = unsafe_hash if isinstance(on_setattr, (list, tuple)): on_setattr = setters.pipe(*on_setattr) @@ -1519,14 +1537,14 @@ def wrap(cls): builder.add_setattr() + nonlocal hash if ( - hash_ is None + hash is None and auto_detect is True and _has_own_attribute(cls, "__hash__") ): hash = False - else: - hash = hash_ + if hash is not True and hash is not False and hash is not None: # Can't use `hash in` because 1 == True for example. raise TypeError( @@ -1604,12 +1622,10 @@ def _generate_unique_filename(cls, func_name): """ Create a "filename" suitable for a function being generated. """ - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), + return ( + f"" ) - return unique_filename def _make_hash(cls, attrs, frozen, cache_hash): @@ -1651,34 +1667,34 @@ def append_hash_computation_lines(prefix, indent): method_lines.extend( [ indent + prefix + hash_func, - indent + " %d," % (type_hash,), + indent + f" {type_hash},", ] ) for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" globs[cmp_name] = a.eq_key method_lines.append( - indent + " %s(self.%s)," % (cmp_name, a.name) + indent + f" {cmp_name}(self.{a.name})," ) else: - method_lines.append(indent + " self.%s," % a.name) + method_lines.append(indent + f" self.{a.name},") method_lines.append(indent + " " + closing_braces) if cache_hash: - method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + method_lines.append(tab + f"if self.{_hash_cache_field} is None:") if frozen: append_hash_computation_lines( - "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + f"object.__setattr__(self, '{_hash_cache_field}', ", tab * 2 ) method_lines.append(tab * 2 + ")") # close __setattr__ else: append_hash_computation_lines( - "self.%s = " % _hash_cache_field, tab * 2 + f"self.{_hash_cache_field} = ", tab * 2 ) - method_lines.append(tab + "return self.%s" % _hash_cache_field) + method_lines.append(tab + f"return self.{_hash_cache_field}") else: append_hash_computation_lines("return ", tab) @@ -1734,27 +1750,15 @@ def _make_eq(cls, attrs): others = [" ) == ("] for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" # Add the key function to the global namespace # of the evaluated function. globs[cmp_name] = a.eq_key - lines.append( - " %s(self.%s)," - % ( - cmp_name, - a.name, - ) - ) - others.append( - " %s(other.%s)," - % ( - cmp_name, - a.name, - ) - ) + lines.append(f" {cmp_name}(self.{a.name}),") + others.append(f" {cmp_name}(other.{a.name}),") else: - lines.append(" self.%s," % (a.name,)) - others.append(" other.%s," % (a.name,)) + lines.append(f" self.{a.name},") + others.append(f" other.{a.name},") lines += others + [" )"] else: @@ -1834,126 +1838,61 @@ def _add_eq(cls, attrs=None): return cls -if HAS_F_STRINGS: - - def _make_repr(attrs, ns, cls): - unique_filename = _generate_unique_filename(cls, "repr") - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, (repr if a.repr is True else a.repr), a.init) - for a in attrs - if a.repr is not False +def _make_repr(attrs, ns, cls): + unique_filename = _generate_unique_filename(cls, "repr") + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' ) - globs = { - name + "_repr": r - for name, r, _ in attr_names_with_reprs - if r != repr - } - globs["_compat"] = _compat - globs["AttributeError"] = AttributeError - globs["NOTHING"] = NOTHING - attribute_fragments = [] - for name, r, i in attr_names_with_reprs: - accessor = ( - "self." + name - if i - else 'getattr(self, "' + name + '", NOTHING)' - ) - fragment = ( - "%s={%s!r}" % (name, accessor) - if r == repr - else "%s={%s_repr(%s)}" % (name, name, accessor) - ) - attribute_fragments.append(fragment) - repr_fragment = ", ".join(attribute_fragments) - - if ns is None: - cls_name_fragment = ( - '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' - ) - else: - cls_name_fragment = ns + ".{self.__class__.__name__}" - - lines = [ - "def __repr__(self):", - " try:", - " already_repring = _compat.repr_context.already_repring", - " except AttributeError:", - " already_repring = {id(self),}", - " _compat.repr_context.already_repring = already_repring", - " else:", - " if id(self) in already_repring:", - " return '...'", - " else:", - " already_repring.add(id(self))", - " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), - " finally:", - " already_repring.remove(id(self))", - ] - - return _make_method( - "__repr__", "\n".join(lines), unique_filename, globs=globs + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) -else: - - def _make_repr(attrs, ns, _): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the - full name. - """ - - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, repr if a.repr is True else a.repr) - for a in attrs - if a.repr is not False - ) - - def __repr__(self): - """ - Automatically created by attrs. - """ - try: - already_repring = _compat.repr_context.already_repring - except AttributeError: - already_repring = set() - _compat.repr_context.already_repring = already_repring - - if id(self) in already_repring: - return "..." - real_cls = self.__class__ - if ns is None: - class_name = real_cls.__qualname__.rsplit(">.", 1)[-1] - else: - class_name = ns + "." + real_cls.__name__ + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" - # Since 'self' remains on the stack (i.e.: strongly referenced) - # for the duration of this call, it's safe to depend on id(...) - # stability, and not need to track the instance and therefore - # worry about properties like weakref- or hash-ability. - already_repring.add(id(self)) - try: - result = [class_name, "("] - first = True - for name, attr_repr in attr_names_with_reprs: - if first: - first = False - else: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - already_repring.remove(id(self)) + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + f" return f'{cls_name_fragment}({repr_fragment})'", + " finally:", + " already_repring.remove(id(self))", + ] - return __repr__ + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) def _add_repr(cls, ns=None, attrs=None): @@ -1969,7 +1908,7 @@ def _add_repr(cls, ns=None, attrs=None): def fields(cls): """ - Return the tuple of ``attrs`` attributes for a class. + Return the tuple of *attrs* attributes for a class. The tuple also allows accessing the fields by their names (see below for examples). @@ -1977,39 +1916,48 @@ def fields(cls): :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. :rtype: tuple (with name accessors) of `attrs.Attribute` - .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields - by name. + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. """ - if not isinstance(cls, type): + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs + raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.") + return attrs def fields_dict(cls): """ - Return an ordered dictionary of ``attrs`` attributes for a class, whose + Return an ordered dictionary of *attrs* attributes for a class, whose keys are the attribute names. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. - :rtype: an ordered dict where keys are attribute names and values are - `attrs.Attribute`\\ s. This will be a `dict` if it's - naturally ordered like on Python 3.6+ or an - :class:`~collections.OrderedDict` otherwise. + :rtype: dict .. versionadded:: 18.1.0 """ @@ -2017,10 +1965,8 @@ def fields_dict(cls): raise TypeError("Passed object must be a class.") attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return ordered_dict((a.name, a) for a in attrs) + raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.") + return {a.name: a for a in attrs} def validate(inst): @@ -2029,7 +1975,7 @@ def validate(inst): Leaves all exceptions through. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. """ if _config._run_validators is False: return @@ -2100,6 +2046,7 @@ def _make_init( cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ) @@ -2112,7 +2059,7 @@ def _make_init( if needs_cached_setattr: # Save the lookup overhead in __init__ if we need to circumvent # setattr hooks. - globs["_setattr"] = _obj_setattr + globs["_cached_setattr_get"] = _obj_setattr.__get__ init = _make_method( "__attrs_init__" if attrs_init else "__init__", @@ -2129,7 +2076,7 @@ def _setattr(attr_name, value_var, has_on_setattr): """ Use the cached object.setattr to set *attr_name* to *value_var*. """ - return "_setattr(self, '%s', %s)" % (attr_name, value_var) + return f"_setattr('{attr_name}', {value_var})" def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2137,7 +2084,7 @@ def _setattr_with_converter(attr_name, value_var, has_on_setattr): Use the cached object.setattr to set *attr_name* to *value_var*, but run its converter first. """ - return "_setattr(self, '%s', %s(%s))" % ( + return "_setattr('%s', %s(%s))" % ( attr_name, _init_converter_pat % (attr_name,), value_var, @@ -2152,7 +2099,7 @@ def _assign(attr_name, value, has_on_setattr): if has_on_setattr: return _setattr(attr_name, value, True) - return "self.%s = %s" % (attr_name, value) + return f"self.{attr_name} = {value}" def _assign_with_converter(attr_name, value_var, has_on_setattr): @@ -2179,6 +2126,7 @@ def _attrs_to_init_script( cache_hash, base_attr_map, is_exc, + needs_cached_setattr, has_cls_on_setattr, attrs_init, ): @@ -2194,6 +2142,14 @@ def _attrs_to_init_script( if pre_init: lines.append("self.__attrs_pre_init__()") + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. + # Note _setattr will be used again below if cache_hash is True + "_setattr = _cached_setattr_get(self)" + ) + if frozen is True: if slots is True: fmt_setter = _setattr @@ -2209,7 +2165,7 @@ def fmt_setter(attr_name, value_var, has_on_setattr): if _is_slot_attr(attr_name, base_attr_map): return _setattr(attr_name, value_var, has_on_setattr) - return "_inst_dict['%s'] = %s" % (attr_name, value_var) + return f"_inst_dict['{attr_name}'] = {value_var}" def fmt_setter_with_converter( attr_name, value_var, has_on_setattr @@ -2247,7 +2203,9 @@ def fmt_setter_with_converter( has_on_setattr = a.on_setattr is not None or ( a.on_setattr is not setters.NO_OP and has_cls_on_setattr ) - arg_name = a.name.lstrip("_") + # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not + # explicitly provided + arg_name = a.alias has_factory = isinstance(a.default, Factory) if has_factory and a.default.takes_self: @@ -2257,12 +2215,12 @@ def fmt_setter_with_converter( if a.init is False: if has_factory: - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( fmt_setter_with_converter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2272,7 +2230,7 @@ def fmt_setter_with_converter( lines.append( fmt_setter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2282,7 +2240,7 @@ def fmt_setter_with_converter( lines.append( fmt_setter_with_converter( attr_name, - "attr_dict['%s'].default" % (attr_name,), + f"attr_dict['{attr_name}'].default", has_on_setattr, ) ) @@ -2292,12 +2250,12 @@ def fmt_setter_with_converter( lines.append( fmt_setter( attr_name, - "attr_dict['%s'].default" % (attr_name,), + f"attr_dict['{attr_name}'].default", has_on_setattr, ) ) elif a.default is not NOTHING and not has_factory: - arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + arg = f"{arg_name}=attr_dict['{attr_name}'].default" if a.kw_only: kw_only_args.append(arg) else: @@ -2316,14 +2274,14 @@ def fmt_setter_with_converter( lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) elif has_factory: - arg = "%s=NOTHING" % (arg_name,) + arg = f"{arg_name}=NOTHING" if a.kw_only: kw_only_args.append(arg) else: args.append(arg) - lines.append("if %s is not NOTHING:" % (arg_name,)) + lines.append(f"if {arg_name} is not NOTHING:") - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( " " @@ -2390,9 +2348,7 @@ def fmt_setter_with_converter( for a in attrs_to_validate: val_name = "__attr_validator_" + a.name attr_name = "__attr_" + a.name - lines.append( - " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) - ) + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a @@ -2408,7 +2364,7 @@ def fmt_setter_with_converter( if frozen: if slots: # if frozen and slots, then _setattr defined above - init_hash_cache = "_setattr(self, '%s', %s)" + init_hash_cache = "_setattr('%s', %s)" else: # if frozen and not slots, then _inst_dict defined above init_hash_cache = "_inst_dict['%s'] = %s" @@ -2419,9 +2375,9 @@ def fmt_setter_with_converter( # For exceptions we rely on BaseException.__init__ for proper # initialization. if is_exc: - vals = ",".join("self." + a.name for a in attrs if a.init) + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) - lines.append("BaseException.__init__(self, %s)" % (vals,)) + lines.append(f"BaseException.__init__(self, {vals})") args = ", ".join(args) if kw_only_args: @@ -2429,29 +2385,45 @@ def fmt_setter_with_converter( ", " if args else "", # leading comma ", ".join(kw_only_args), # kw_only args ) + return ( - """\ -def {init_name}(self, {args}): - {lines} -""".format( - init_name=("__attrs_init__" if attrs_init else "__init__"), - args=args, - lines="\n ".join(lines) if lines else "pass", + "def %s(self, %s):\n %s\n" + % ( + ("__attrs_init__" if attrs_init else "__init__"), + args, + "\n ".join(lines) if lines else "pass", ), names_for_globals, annotations, ) +def _default_init_alias_for(name: str) -> str: + """ + The default __init__ parameter name for a field. + + This performs private-name adjustment via leading-unscore stripping, + and is the default value of Attribute.alias if not provided. + """ + + return name.lstrip("_") + + class Attribute: """ *Read-only* representation of an attribute. + .. warning:: + + You should never instantiate this class yourself. + The class has *all* arguments of `attr.ib` (except for ``factory`` which is only syntactic sugar for ``default=Factory(...)`` plus the following: - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables @@ -2467,12 +2439,16 @@ class Attribute: - Validators get them passed as the first argument. - The :ref:`field transformer ` hook receives a list of them. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + .. versionadded:: 20.1.0 *inherited* .. versionadded:: 20.1.0 *on_setattr* .. versionchanged:: 20.2.0 *inherited* is not taken into account for equality checks and hashing anymore. .. versionadded:: 21.1.0 *eq_key* and *order_key* + .. versionadded:: 22.2.0 *alias* For the full version history of the fields, see `attr.ib`. """ @@ -2494,6 +2470,7 @@ class Attribute: "kw_only", "inherited", "on_setattr", + "alias", ) def __init__( @@ -2515,13 +2492,14 @@ def __init__( order=None, order_key=None, on_setattr=None, + alias=None, ): eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq_key or eq, order_key or order, True ) # Cache this descriptor here to speed things up later. - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) # Despite the big red warning, people *do* instantiate `Attribute` # themselves. @@ -2548,6 +2526,7 @@ def __init__( bound_setattr("kw_only", kw_only) bound_setattr("inherited", inherited) bound_setattr("on_setattr", on_setattr) + bound_setattr("alias", alias) def __setattr__(self, name, value): raise FrozenInstanceError() @@ -2580,16 +2559,16 @@ def from_counting_attr(cls, name, ca, type=None): type=type, cmp=None, inherited=False, - **inst_dict + **inst_dict, ) - # Don't use attr.evolve since fields(Attribute) doesn't work + # Don't use attrs.evolve since fields(Attribute) doesn't work def evolve(self, **changes): """ Copy *self* and apply *changes*. - This works similarly to `attr.evolve` but that function does not work - with ``Attribute``. + This works similarly to `attrs.evolve` but that function does not work + with `Attribute`. It is mainly meant to be used for `transform-fields`. @@ -2618,7 +2597,7 @@ def __setstate__(self, state): self._setattrs(zip(self.__slots__, state)) def _setattrs(self, name_values_pairs): - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) for name, value in name_values_pairs: if name != "metadata": bound_setattr(name, value) @@ -2643,6 +2622,7 @@ def _setattrs(self, name_values_pairs): hash=(name != "metadata"), init=True, inherited=False, + alias=_default_init_alias_for(name), ) for name in Attribute.__slots__ ] @@ -2681,10 +2661,12 @@ class _CountingAttr: "type", "kw_only", "on_setattr", + "alias", ) __attrs_attrs__ = tuple( Attribute( name=name, + alias=_default_init_alias_for(name), default=NOTHING, validator=None, repr=True, @@ -2708,10 +2690,12 @@ class _CountingAttr: "hash", "init", "on_setattr", + "alias", ) ) + ( Attribute( name="metadata", + alias="metadata", default=None, validator=None, repr=True, @@ -2746,6 +2730,7 @@ def __init__( order, order_key, on_setattr, + alias, ): _CountingAttr.cls_counter += 1 self.counter = _CountingAttr.cls_counter @@ -2763,6 +2748,7 @@ def __init__( self.type = type self.kw_only = kw_only self.on_setattr = on_setattr + self.alias = alias def validator(self, meth): """ @@ -2817,10 +2803,6 @@ class Factory: __slots__ = ("factory", "takes_self") def __init__(self, factory, takes_self=False): - """ - `Factory` is part of the default machinery so if we want a default - value here, we have to implement it ourselves. - """ self.factory = factory self.takes_self = takes_self @@ -2858,18 +2840,17 @@ def __setstate__(self, state): def make_class(name, attrs, bases=(object,), **attributes_arguments): - """ + r""" A quick way to create a new class called *name* with *attrs*. :param str name: The name for the new class. :param attrs: A list of names or a dictionary of mappings of names to - attributes. + `attr.ib`\ s / `attrs.field`\ s. - If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the names or attributes inside *attrs*. Otherwise the - order of the definition of the attributes is used. + The order is deduced from the order of the names or attributes inside + *attrs*. Otherwise the order of the definition of the attributes is + used. :type attrs: `list` or `dict` :param tuple bases: Classes that the new class will subclass. diff --git a/pipenv/vendor/attr/_next_gen.py b/pipenv/vendor/attr/_next_gen.py index 5a06a74385..8f7c0b9a46 100644 --- a/pipenv/vendor/attr/_next_gen.py +++ b/pipenv/vendor/attr/_next_gen.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: MIT """ -These are Python 3.6+-only and keyword-only APIs that call `attr.s` and -`attr.ib` with different default values. +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. """ @@ -26,6 +26,7 @@ def define( *, these=None, repr=None, + unsafe_hash=None, hash=None, init=None, slots=True, @@ -45,7 +46,7 @@ def define( match_args=True, ): r""" - Define an ``attrs`` class. + Define an *attrs* class. Differences to the classic `attr.s` that it uses underneath: @@ -81,6 +82,8 @@ def define( .. versionadded:: 20.1.0 .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). """ def do_it(cls, auto_attribs): @@ -89,6 +92,7 @@ def do_it(cls, auto_attribs): these=these, repr=repr, hash=hash, + unsafe_hash=unsafe_hash, init=init, slots=slots, frozen=frozen, @@ -163,17 +167,23 @@ def field( hash=None, init=True, metadata=None, + type=None, converter=None, factory=None, kw_only=False, eq=None, order=None, on_setattr=None, + alias=None, ): """ Identical to `attr.ib`, except keyword-only and with some arguments removed. + .. versionadded:: 23.1.0 + The *type* parameter has been re-added; mostly for + {func}`attrs.make_class`. Please note that type checkers ignore this + metadata. .. versionadded:: 20.1.0 """ return attrib( @@ -183,12 +193,14 @@ def field( hash=hash, init=init, metadata=metadata, + type=type, converter=converter, factory=factory, kw_only=kw_only, eq=eq, order=order, on_setattr=on_setattr, + alias=alias, ) diff --git a/pipenv/vendor/attr/_typing_compat.pyi b/pipenv/vendor/attr/_typing_compat.pyi new file mode 100644 index 0000000000..ca7b71e906 --- /dev/null +++ b/pipenv/vendor/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/pipenv/vendor/attr/converters.py b/pipenv/vendor/attr/converters.py index a73626c26d..4cada106b0 100644 --- a/pipenv/vendor/attr/converters.py +++ b/pipenv/vendor/attr/converters.py @@ -141,4 +141,4 @@ def to_bool(val): except TypeError: # Raised when "val" is not hashable (e.g., lists) pass - raise ValueError("Cannot convert value to bool: {}".format(val)) + raise ValueError(f"Cannot convert value to bool: {val}") diff --git a/pipenv/vendor/attr/converters.pyi b/pipenv/vendor/attr/converters.pyi index 0f58088a37..5abb49f6d5 100644 --- a/pipenv/vendor/attr/converters.pyi +++ b/pipenv/vendor/attr/converters.pyi @@ -1,4 +1,4 @@ -from typing import Callable, Optional, TypeVar, overload +from typing import Callable, TypeVar, overload from . import _ConverterType diff --git a/pipenv/vendor/attr/exceptions.py b/pipenv/vendor/attr/exceptions.py index 5dc51e0a82..2883493085 100644 --- a/pipenv/vendor/attr/exceptions.py +++ b/pipenv/vendor/attr/exceptions.py @@ -34,7 +34,7 @@ class FrozenAttributeError(FrozenError): class AttrsAttributeNotFoundError(ValueError): """ - An ``attrs`` function couldn't find an attribute that the user asked for. + An *attrs* function couldn't find an attribute that the user asked for. .. versionadded:: 16.2.0 """ @@ -42,7 +42,7 @@ class AttrsAttributeNotFoundError(ValueError): class NotAnAttrsClassError(ValueError): """ - A non-``attrs`` class has been passed into an ``attrs`` function. + A non-*attrs* class has been passed into an *attrs* function. .. versionadded:: 16.2.0 """ @@ -50,7 +50,7 @@ class NotAnAttrsClassError(ValueError): class DefaultAlreadySetError(RuntimeError): """ - A default has been set using ``attr.ib()`` and is attempted to be reset + A default has been set when defining the field and is attempted to be reset using the decorator. .. versionadded:: 17.1.0 @@ -59,8 +59,7 @@ class DefaultAlreadySetError(RuntimeError): class UnannotatedAttributeError(RuntimeError): """ - A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type - annotation. + A class with ``auto_attribs=True`` has a field without a type annotation. .. versionadded:: 17.3.0 """ @@ -68,7 +67,7 @@ class UnannotatedAttributeError(RuntimeError): class PythonTooOldError(RuntimeError): """ - It was attempted to use an ``attrs`` feature that requires a newer Python + It was attempted to use an *attrs* feature that requires a newer Python version. .. versionadded:: 18.2.0 @@ -77,8 +76,8 @@ class PythonTooOldError(RuntimeError): class NotCallableError(TypeError): """ - A ``attr.ib()`` requiring a callable has been set with a value - that is not callable. + A field requiring a callable has been set with a value that is not + callable. .. versionadded:: 19.2.0 """ diff --git a/pipenv/vendor/attr/filters.py b/pipenv/vendor/attr/filters.py index baa25e9465..a1e40c98db 100644 --- a/pipenv/vendor/attr/filters.py +++ b/pipenv/vendor/attr/filters.py @@ -13,6 +13,7 @@ def _split_what(what): """ return ( frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), frozenset(cls for cls in what if isinstance(cls, Attribute)), ) @@ -22,14 +23,21 @@ def include(*what): Include *what*. :param what: What to include. - :type what: `list` of `type` or `attrs.Attribute`\\ s + :type what: `list` of classes `type`, field names `str` or + `attrs.Attribute`\\ s :rtype: `callable` + + .. versionchanged:: 23.1.0 Accept strings with field names. """ - cls, attrs = _split_what(what) + cls, names, attrs = _split_what(what) def include_(attribute, value): - return value.__class__ in cls or attribute in attrs + return ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) return include_ @@ -39,13 +47,20 @@ def exclude(*what): Exclude *what*. :param what: What to exclude. - :type what: `list` of classes or `attrs.Attribute`\\ s. + :type what: `list` of classes `type`, field names `str` or + `attrs.Attribute`\\ s. :rtype: `callable` + + .. versionchanged:: 23.3.0 Accept field name string as input argument """ - cls, attrs = _split_what(what) + cls, names, attrs = _split_what(what) def exclude_(attribute, value): - return value.__class__ not in cls and attribute not in attrs + return not ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) return exclude_ diff --git a/pipenv/vendor/attr/filters.pyi b/pipenv/vendor/attr/filters.pyi index 993866865e..8a02fa0fc0 100644 --- a/pipenv/vendor/attr/filters.pyi +++ b/pipenv/vendor/attr/filters.pyi @@ -2,5 +2,5 @@ from typing import Any, Union from . import Attribute, _FilterType -def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... -def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... +def include(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... +def exclude(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/pipenv/vendor/attr/setters.pyi b/pipenv/vendor/attr/setters.pyi index 3f5603c2b0..72f7ce4761 100644 --- a/pipenv/vendor/attr/setters.pyi +++ b/pipenv/vendor/attr/setters.pyi @@ -1,4 +1,4 @@ -from typing import Any, NewType, NoReturn, TypeVar, cast +from typing import Any, NewType, NoReturn, TypeVar from . import Attribute, _OnSetAttrType diff --git a/pipenv/vendor/attr/validators.py b/pipenv/vendor/attr/validators.py index eece517da8..1488554f78 100644 --- a/pipenv/vendor/attr/validators.py +++ b/pipenv/vendor/attr/validators.py @@ -9,18 +9,14 @@ import re from contextlib import contextmanager +from re import Pattern from ._config import get_run_validators, set_run_validators from ._make import _AndValidator, and_, attrib, attrs +from .converters import default_if_none from .exceptions import NotCallableError -try: - Pattern = re.Pattern -except AttributeError: # Python <3.7 lacks a Pattern type. - Pattern = type(re.compile("")) - - __all__ = [ "and_", "deep_iterable", @@ -37,6 +33,7 @@ "matches_re", "max_len", "min_len", + "not_", "optional", "provides", "set_disabled", @@ -126,7 +123,7 @@ def instance_of(type): `isinstance` therefore it's also valid to pass a tuple of types). :param type: The type to check for. - :type type: type or tuple of types + :type type: type or tuple of type :raises TypeError: With a human readable error message, the attribute (of type `attrs.Attribute`), the expected type, and the value it @@ -247,7 +244,17 @@ def provides(interface): :raises TypeError: With a human readable error message, the attribute (of type `attrs.Attribute`), the expected interface, and the value it got. + + .. deprecated:: 23.1.0 """ + import warnings + + warnings.warn( + "attrs's zope-interface support is deprecated and will be removed in, " + "or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) return _ProvidesValidator(interface) @@ -273,15 +280,16 @@ def optional(validator): which can be set to ``None`` in addition to satisfying the requirements of the sub-validator. - :param validator: A validator (or a list of validators) that is used for - non-``None`` values. - :type validator: callable or `list` of callables. + :param Callable | tuple[Callable] | list[Callable] validator: A validator + (or validators) that is used for non-``None`` values. .. versionadded:: 15.1.0 .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. """ - if isinstance(validator, list): + if isinstance(validator, (list, tuple)): return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) @@ -357,13 +365,13 @@ def __repr__(self): def is_callable(): """ - A validator that raises a `attr.exceptions.NotCallableError` if the + A validator that raises a `attrs.exceptions.NotCallableError` if the initializer is called with a value for this particular attribute that is not callable. .. versionadded:: 19.1.0 - :raises `attr.exceptions.NotCallableError`: With a human readable error + :raises attrs.exceptions.NotCallableError: With a human readable error message containing the attribute (`attrs.Attribute`) name, and the value it got. """ @@ -391,7 +399,7 @@ def __repr__(self): iterable_identifier = ( "" if self.iterable_validator is None - else " {iterable!r}".format(iterable=self.iterable_validator) + else f" {self.iterable_validator!r}" ) return ( "".format(max=self.max_length) + return f"" def max_len(length): @@ -579,7 +587,7 @@ def __call__(self, inst, attr, value): ) def __repr__(self): - return "".format(min=self.min_length) + return f"" def min_len(length): @@ -592,3 +600,121 @@ def min_len(length): .. versionadded:: 22.1.0 """ return _MinLengthValidator(length) + + +@attrs(repr=False, slots=True, hash=True) +class _SubclassOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not issubclass(value, self.type): + raise TypeError( + "'{name}' must be a subclass of {type!r} " + "(got {value!r}).".format( + name=attr.name, + type=self.type, + value=value, + ), + attr, + self.type, + value, + ) + + def __repr__(self): + return "".format( + type=self.type + ) + + +def _subclass_of(type): + """ + A validator that raises a `TypeError` if the initializer is called + with a wrong type for this particular attribute (checks are performed using + `issubclass` therefore it's also valid to pass a tuple of types). + + :param type: The type to check for. + :type type: type or tuple of types + + :raises TypeError: With a human readable error message, the attribute + (of type `attrs.Attribute`), the expected type, and the value it + got. + """ + return _SubclassOfValidator(type) + + +@attrs(repr=False, slots=True, hash=True) +class _NotValidator: + validator = attrib() + msg = attrib( + converter=default_if_none( + "not_ validator child '{validator!r}' " + "did not raise a captured error" + ) + ) + exc_types = attrib( + validator=deep_iterable( + member_validator=_subclass_of(Exception), + iterable_validator=instance_of(tuple), + ), + ) + + def __call__(self, inst, attr, value): + try: + self.validator(inst, attr, value) + except self.exc_types: + pass # suppress error to invert validity + else: + raise ValueError( + self.msg.format( + validator=self.validator, + exc_types=self.exc_types, + ), + attr, + self.validator, + value, + self.exc_types, + ) + + def __repr__(self): + return ( + "" + ).format( + what=self.validator, + exc_types=self.exc_types, + ) + + +def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): + """ + A validator that wraps and logically 'inverts' the validator passed to it. + It will raise a `ValueError` if the provided validator *doesn't* raise a + `ValueError` or `TypeError` (by default), and will suppress the exception + if the provided validator *does*. + + Intended to be used with existing validators to compose logic without + needing to create inverted variants, for example, ``not_(in_(...))``. + + :param validator: A validator to be logically inverted. + :param msg: Message to raise if validator fails. + Formatted with keys ``exc_types`` and ``validator``. + :type msg: str + :param exc_types: Exception type(s) to capture. + Other types raised by child validators will not be intercepted and + pass through. + + :raises ValueError: With a human readable error message, + the attribute (of type `attrs.Attribute`), + the validator that failed to raise an exception, + the value it got, + and the expected exception types. + + .. versionadded:: 22.2.0 + """ + try: + exc_types = tuple(exc_types) + except TypeError: + exc_types = (exc_types,) + return _NotValidator(validator, msg, exc_types) diff --git a/pipenv/vendor/attr/validators.pyi b/pipenv/vendor/attr/validators.pyi index 54b9dba24e..d194a75abc 100644 --- a/pipenv/vendor/attr/validators.pyi +++ b/pipenv/vendor/attr/validators.pyi @@ -51,7 +51,9 @@ def instance_of( def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... def provides(interface: Any) -> _ValidatorType[Any]: ... def optional( - validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] + validator: Union[ + _ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T]] + ] ) -> _ValidatorType[Optional[_T]]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... @@ -78,3 +80,9 @@ def ge(val: _T) -> _ValidatorType[_T]: ... def gt(val: _T) -> _ValidatorType[_T]: ... def max_len(length: int) -> _ValidatorType[_T]: ... def min_len(length: int) -> _ValidatorType[_T]: ... +def not_( + validator: _ValidatorType[_T], + *, + msg: Optional[str] = None, + exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ..., +) -> _ValidatorType[_T]: ... diff --git a/pipenv/vendor/attrs/LICENSE b/pipenv/vendor/attrs/LICENSE deleted file mode 100644 index 2bd6453d25..0000000000 --- a/pipenv/vendor/attrs/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Hynek Schlawack and the attrs contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/pipenv/vendor/attrs/__init__.py b/pipenv/vendor/attrs/__init__.py index a6a3db2026..9175eb0189 100644 --- a/pipenv/vendor/attrs/__init__.py +++ b/pipenv/vendor/attrs/__init__.py @@ -3,17 +3,9 @@ from pipenv.vendor.attr import ( NOTHING, Attribute, + AttrsInstance, Factory, - __author__, - __copyright__, - __description__, - __doc__, - __email__, - __license__, - __title__, - __url__, - __version__, - __version_info__, + _make_getattr, assoc, cmp_using, define, @@ -48,6 +40,7 @@ "assoc", "astuple", "Attribute", + "AttrsInstance", "cmp_using", "converters", "define", @@ -68,3 +61,5 @@ "validate", "validators", ] + +__getattr__ = _make_getattr(__name__) diff --git a/pipenv/vendor/attrs/__init__.pyi b/pipenv/vendor/attrs/__init__.pyi index fc44de46a0..9372cfea16 100644 --- a/pipenv/vendor/attrs/__init__.pyi +++ b/pipenv/vendor/attrs/__init__.pyi @@ -23,6 +23,7 @@ from attr import __version_info__ as __version_info__ from attr import _FilterType from attr import assoc as assoc from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance from attr import cmp_using as cmp_using from attr import converters as converters from attr import define as define @@ -45,7 +46,7 @@ from attr import validators as validators # TODO: see definition of attr.asdict/astuple def asdict( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., dict_factory: Type[Mapping[Any, Any]] = ..., @@ -58,7 +59,7 @@ def asdict( # TODO: add support for returning NamedTuple from the mypy plugin def astuple( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., tuple_factory: Type[Sequence[Any]] = ..., diff --git a/pipenv/vendor/click/__init__.py b/pipenv/vendor/click/__init__.py index a2ed5d1357..e3ef423b61 100644 --- a/pipenv/vendor/click/__init__.py +++ b/pipenv/vendor/click/__init__.py @@ -41,7 +41,6 @@ from .termui import confirm as confirm from .termui import echo_via_pager as echo_via_pager from .termui import edit as edit -from .termui import get_terminal_size as get_terminal_size from .termui import getchar as getchar from .termui import launch as launch from .termui import pause as pause @@ -68,8 +67,7 @@ from .utils import format_filename as format_filename from .utils import get_app_dir as get_app_dir from .utils import get_binary_stream as get_binary_stream -from .utils import get_os_args as get_os_args from .utils import get_text_stream as get_text_stream from .utils import open_file as open_file -__version__ = "8.0.3" +__version__ = "8.1.3" diff --git a/pipenv/vendor/click/_compat.py b/pipenv/vendor/click/_compat.py index 7877b52932..b80dcf2d22 100644 --- a/pipenv/vendor/click/_compat.py +++ b/pipenv/vendor/click/_compat.py @@ -388,9 +388,9 @@ def open_stream( ) -> t.Tuple[t.IO, bool]: binary = "b" in mode - # Standard streams first. These are simple because they don't need - # special handling for the atomic flag. It's entirely ignored. - if filename == "-": + # Standard streams first. These are simple because they ignore the + # atomic flag. Use fsdecode to handle Path("-"). + if os.fsdecode(filename) == "-": if any(m in mode for m in ["w", "a", "x"]): if binary: return get_binary_stdout(), False @@ -561,7 +561,6 @@ def _safe_write(s): return rv - else: def _get_argv_encoding() -> str: diff --git a/pipenv/vendor/click/_termui_impl.py b/pipenv/vendor/click/_termui_impl.py index 39c1d08f4e..4b979bcc1e 100644 --- a/pipenv/vendor/click/_termui_impl.py +++ b/pipenv/vendor/click/_termui_impl.py @@ -675,7 +675,6 @@ def getchar(echo: bool) -> str: _translate_ch_to_exc(rv) return rv - else: import tty import termios diff --git a/pipenv/vendor/click/_unicodefun.py b/pipenv/vendor/click/_unicodefun.py deleted file mode 100644 index 9cb30c384f..0000000000 --- a/pipenv/vendor/click/_unicodefun.py +++ /dev/null @@ -1,100 +0,0 @@ -import codecs -import os -from gettext import gettext as _ - - -def _verify_python_env() -> None: - """Ensures that the environment is good for Unicode.""" - try: - from locale import getpreferredencoding - - fs_enc = codecs.lookup(getpreferredencoding()).name - except Exception: - fs_enc = "ascii" - - if fs_enc != "ascii": - return - - extra = [ - _( - "Click will abort further execution because Python was" - " configured to use ASCII as encoding for the environment." - " Consult https://click.palletsprojects.com/unicode-support/" - " for mitigation steps." - ) - ] - - if os.name == "posix": - import subprocess - - try: - rv = subprocess.Popen( - ["locale", "-a"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="ascii", - errors="replace", - ).communicate()[0] - except OSError: - rv = "" - - good_locales = set() - has_c_utf8 = False - - for line in rv.splitlines(): - locale = line.strip() - - if locale.lower().endswith((".utf-8", ".utf8")): - good_locales.add(locale) - - if locale.lower() in ("c.utf8", "c.utf-8"): - has_c_utf8 = True - - if not good_locales: - extra.append( - _( - "Additional information: on this system no suitable" - " UTF-8 locales were discovered. This most likely" - " requires resolving by reconfiguring the locale" - " system." - ) - ) - elif has_c_utf8: - extra.append( - _( - "This system supports the C.UTF-8 locale which is" - " recommended. You might be able to resolve your" - " issue by exporting the following environment" - " variables:" - ) - ) - extra.append(" export LC_ALL=C.UTF-8\n export LANG=C.UTF-8") - else: - extra.append( - _( - "This system lists some UTF-8 supporting locales" - " that you can pick from. The following suitable" - " locales were discovered: {locales}" - ).format(locales=", ".join(sorted(good_locales))) - ) - - bad_locale = None - - for env_locale in os.environ.get("LC_ALL"), os.environ.get("LANG"): - if env_locale and env_locale.lower().endswith((".utf-8", ".utf8")): - bad_locale = env_locale - - if env_locale is not None: - break - - if bad_locale is not None: - extra.append( - _( - "Click discovered that you exported a UTF-8 locale" - " but the locale system could not pick up from it" - " because it does not exist. The exported locale is" - " {locale!r} but it is not supported." - ).format(locale=bad_locale) - ) - - raise RuntimeError("\n\n".join(extra)) diff --git a/pipenv/vendor/click/core.py b/pipenv/vendor/click/core.py index 56f05a31ea..66bd1d9823 100644 --- a/pipenv/vendor/click/core.py +++ b/pipenv/vendor/click/core.py @@ -1,8 +1,8 @@ import enum import errno +import inspect import os import sys -import typing import typing as t from collections import abc from contextlib import contextmanager @@ -14,7 +14,6 @@ from itertools import repeat from . import types -from ._unicodefun import _verify_python_env from .exceptions import Abort from .exceptions import BadParameter from .exceptions import ClickException @@ -224,9 +223,14 @@ class Context: codes are used in texts that Click prints which is by default not the case. This for instance would affect help output. - :param show_default: Show defaults for all options. If not set, - defaults to the value from a parent context. Overrides an - option's ``show_default`` argument. + :param show_default: Show the default value for commands. If this + value is not set, it defaults to the value from the parent + context. ``Command.show_default`` overrides this default for the + specific command. + + .. versionchanged:: 8.1 + The ``show_default`` parameter is overridden by + ``Command.show_default``, instead of the other way around. .. versionchanged:: 8.0 The ``show_default`` parameter defaults to the value from the @@ -288,6 +292,8 @@ def __init__( #: must be never propagated to another arguments. This is used #: to implement nested parsing. self.protected_args: t.List[str] = [] + #: the collected prefixes of the command's options. + self._opt_prefixes: t.Set[str] = set(parent._opt_prefixes) if parent else set() if obj is None and parent is not None: obj = parent.obj @@ -632,13 +638,13 @@ def ensure_object(self, object_type: t.Type[V]) -> V: self.obj = rv = object_type() return rv - @typing.overload + @t.overload def lookup_default( self, name: str, call: "te.Literal[True]" = True ) -> t.Optional[t.Any]: ... - @typing.overload + @t.overload def lookup_default( self, name: str, call: "te.Literal[False]" = ... ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: @@ -956,7 +962,7 @@ def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionIte return results - @typing.overload + @t.overload def main( self, args: t.Optional[t.Sequence[str]] = None, @@ -967,7 +973,7 @@ def main( ) -> "te.NoReturn": ... - @typing.overload + @t.overload def main( self, args: t.Optional[t.Sequence[str]] = None, @@ -1029,10 +1035,6 @@ def main( .. versionchanged:: 3.0 Added the ``standalone_mode`` parameter. """ - # Verify that the environment is configured correctly, or reject - # further execution to avoid a broken script. - _verify_python_env() - if args is None: args = sys.argv[1:] @@ -1133,13 +1135,6 @@ class Command(BaseCommand): Click. A basic command handles command line parsing and might dispatch more parsing to commands nested below it. - .. versionchanged:: 2.0 - Added the `context_settings` parameter. - .. versionchanged:: 8.0 - Added repr showing the command name - .. versionchanged:: 7.1 - Added the `no_args_is_help` parameter. - :param name: the name of the command to use unless a group overrides it. :param context_settings: an optional dictionary with defaults that are passed to the context object. @@ -1161,6 +1156,20 @@ class Command(BaseCommand): :param deprecated: issues a message indicating that the command is deprecated. + + .. versionchanged:: 8.1 + ``help``, ``epilog``, and ``short_help`` are stored unprocessed, + all formatting is done when outputting help text, not at init, + and is done even if not using the ``@command`` decorator. + + .. versionchanged:: 8.0 + Added a ``repr`` showing the command name. + + .. versionchanged:: 7.1 + Added the ``no_args_is_help`` parameter. + + .. versionchanged:: 2.0 + Added the ``context_settings`` parameter. """ def __init__( @@ -1186,12 +1195,6 @@ def __init__( #: should show up in the help page and execute. Eager parameters #: will automatically be handled before non eager ones. self.params: t.List["Parameter"] = params or [] - - # if a form feed (page break) is found in the help text, truncate help - # text to the content preceding the first form feed - if help and "\f" in help: - help = help.split("\f", 1)[0] - self.help = help self.epilog = epilog self.options_metavar = options_metavar @@ -1299,10 +1302,12 @@ def get_short_help_str(self, limit: int = 45) -> str: """Gets short help for the command or makes it by shortening the long help string. """ - text = self.short_help or "" - - if not text and self.help: + if self.short_help: + text = inspect.cleandoc(self.short_help) + elif self.help: text = make_default_short_help(self.help, limit) + else: + text = "" if self.deprecated: text = _("(Deprecated) {text}").format(text=text) @@ -1328,12 +1333,13 @@ def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: """Writes the help text to the formatter if it exists.""" - text = self.help or "" + text = self.help if self.help is not None else "" if self.deprecated: text = _("(Deprecated) {text}").format(text=text) if text: + text = inspect.cleandoc(text).partition("\f")[0] formatter.write_paragraph() with formatter.indentation(): @@ -1354,9 +1360,11 @@ def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: """Writes the epilog into the formatter if it exists.""" if self.epilog: + epilog = inspect.cleandoc(self.epilog) formatter.write_paragraph() + with formatter.indentation(): - formatter.write_text(self.epilog) + formatter.write_text(epilog) def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: if not args and self.no_args_is_help and not ctx.resilient_parsing: @@ -1379,6 +1387,7 @@ def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: ) ctx.args = args + ctx._opt_prefixes.update(parser._opt_prefixes) return args def invoke(self, ctx: Context) -> t.Any: @@ -1568,17 +1577,6 @@ def function(__value, *args, **kwargs): # type: ignore return decorator - def resultcallback(self, replace: bool = False) -> t.Callable[[F], F]: - import warnings - - warnings.warn( - "'resultcallback' has been renamed to 'result_callback'." - " The old name will be removed in Click 8.1.", - DeprecationWarning, - stacklevel=2, - ) - return self.result_callback(replace=replace) - def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: """Extra format methods for multi methods that adds all the commands after the options. @@ -1631,11 +1629,11 @@ def _process_result(value: t.Any) -> t.Any: if not ctx.protected_args: if self.invoke_without_command: # No subcommand was invoked, so the result callback is - # invoked with None for regular groups, or an empty list - # for chained groups. + # invoked with the group return value for regular + # groups, or an empty list for chained groups. with ctx: - super().invoke(ctx) - return _process_result([] if self.chain else None) + rv = super().invoke(ctx) + return _process_result([] if self.chain else rv) ctx.fail(_("Missing command.")) # Fetch args back out @@ -1811,9 +1809,19 @@ def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None: _check_multicommand(self, name, cmd, register=True) self.commands[name] = cmd + @t.overload + def command(self, __func: t.Callable[..., t.Any]) -> Command: + ... + + @t.overload def command( self, *args: t.Any, **kwargs: t.Any ) -> t.Callable[[t.Callable[..., t.Any]], Command]: + ... + + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], Command], Command]: """A shortcut decorator for declaring and attaching a command to the group. This takes the same arguments as :func:`command` and immediately registers the created command with this group by @@ -1822,24 +1830,49 @@ def command( To customize the command class used, set the :attr:`command_class` attribute. + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + .. versionchanged:: 8.0 Added the :attr:`command_class` attribute. """ from .decorators import command - if self.command_class is not None and "cls" not in kwargs: + if self.command_class and kwargs.get("cls") is None: kwargs["cls"] = self.command_class + func: t.Optional[t.Callable] = None + + if args and callable(args[0]): + assert ( + len(args) == 1 and not kwargs + ), "Use 'command(**kwargs)(callable)' to provide arguments." + (func,) = args + args = () + def decorator(f: t.Callable[..., t.Any]) -> Command: - cmd = command(*args, **kwargs)(f) + cmd: Command = command(*args, **kwargs)(f) self.add_command(cmd) return cmd + if func is not None: + return decorator(func) + return decorator + @t.overload + def group(self, __func: t.Callable[..., t.Any]) -> "Group": + ... + + @t.overload def group( self, *args: t.Any, **kwargs: t.Any ) -> t.Callable[[t.Callable[..., t.Any]], "Group"]: + ... + + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], "Group"], "Group"]: """A shortcut decorator for declaring and attaching a group to the group. This takes the same arguments as :func:`group` and immediately registers the created group with this group by @@ -1848,22 +1881,37 @@ def group( To customize the group class used, set the :attr:`group_class` attribute. + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + .. versionchanged:: 8.0 Added the :attr:`group_class` attribute. """ from .decorators import group - if self.group_class is not None and "cls" not in kwargs: + func: t.Optional[t.Callable] = None + + if args and callable(args[0]): + assert ( + len(args) == 1 and not kwargs + ), "Use 'group(**kwargs)(callable)' to provide arguments." + (func,) = args + args = () + + if self.group_class is not None and kwargs.get("cls") is None: if self.group_class is type: kwargs["cls"] = type(self) else: kwargs["cls"] = self.group_class def decorator(f: t.Callable[..., t.Any]) -> "Group": - cmd = group(*args, **kwargs)(f) + cmd: Group = group(*args, **kwargs)(f) self.add_command(cmd) return cmd + if func is not None: + return decorator(func) + return decorator def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: @@ -2020,11 +2068,6 @@ def __init__( t.Union[t.List["CompletionItem"], t.List[str]], ] ] = None, - autocompletion: t.Optional[ - t.Callable[ - [Context, t.List[str], str], t.List[t.Union[t.Tuple[str, str], str]] - ] - ] = None, ) -> None: self.name, self.opts, self.secondary_opts = self._parse_decls( param_decls or (), expose_value @@ -2048,36 +2091,6 @@ def __init__( self.is_eager = is_eager self.metavar = metavar self.envvar = envvar - - if autocompletion is not None: - import warnings - - warnings.warn( - "'autocompletion' is renamed to 'shell_complete'. The old name is" - " deprecated and will be removed in Click 8.1. See the docs about" - " 'Parameter' for information about new behavior.", - DeprecationWarning, - stacklevel=2, - ) - - def shell_complete( - ctx: Context, param: "Parameter", incomplete: str - ) -> t.List["CompletionItem"]: - from pipenv.vendor.click.shell_completion import CompletionItem - - out = [] - - for c in autocompletion(ctx, [], incomplete): # type: ignore - if isinstance(c, tuple): - c = CompletionItem(c[0], help=c[1]) - elif isinstance(c, str): - c = CompletionItem(c) - - if c.value.startswith(incomplete): - out.append(c) - - return out - self._custom_shell_complete = shell_complete if __debug__: @@ -2172,13 +2185,13 @@ def make_metavar(self) -> str: return metavar - @typing.overload + @t.overload def get_default( self, ctx: Context, call: "te.Literal[True]" = True ) -> t.Optional[t.Any]: ... - @typing.overload + @t.overload def get_default( self, ctx: Context, call: bool = ... ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: @@ -2399,25 +2412,27 @@ class Option(Parameter): All other parameters are passed onwards to the parameter constructor. - :param show_default: controls if the default value should be shown on the - help page. Normally, defaults are not shown. If this - value is a string, it shows the string instead of the - value. This is particularly useful for dynamic options. - :param show_envvar: controls if an environment variable should be shown on - the help page. Normally, environment variables - are not shown. - :param prompt: if set to `True` or a non empty string then the user will be - prompted for input. If set to `True` the prompt will be the - option name capitalized. + :param show_default: Show the default value for this option in its + help text. Values are not shown by default, unless + :attr:`Context.show_default` is ``True``. If this value is a + string, it shows that string in parentheses instead of the + actual value. This is particularly useful for dynamic options. + For single option boolean flags, the default remains hidden if + its value is ``False``. + :param show_envvar: Controls if an environment variable should be + shown on the help page. Normally, environment variables are not + shown. + :param prompt: If set to ``True`` or a non empty string then the + user will be prompted for input. If set to ``True`` the prompt + will be the option name capitalized. :param confirmation_prompt: Prompt a second time to confirm the value if it was prompted for. Can be set to a string instead of ``True`` to customize the message. :param prompt_required: If set to ``False``, the user will be prompted for input only when the option was specified as a flag without a value. - :param hide_input: if this is `True` then the input on the prompt will be - hidden from the user. This is useful for password - input. + :param hide_input: If this is ``True`` then the input on the prompt + will be hidden from the user. This is useful for password input. :param is_flag: forces this option to act as a flag. The default is auto detection. :param flag_value: which value should be used for this flag if it's @@ -2435,6 +2450,18 @@ class Option(Parameter): :param help: the help string. :param hidden: hide this option from help outputs. + .. versionchanged:: 8.1.0 + Help text indentation is cleaned here instead of only in the + ``@option`` decorator. + + .. versionchanged:: 8.1.0 + The ``show_default`` parameter overrides + ``Context.show_default``. + + .. versionchanged:: 8.1.0 + The default of a single option boolean flag is not shown if the + default value is ``False``. + .. versionchanged:: 8.0.1 ``type`` is detected from ``flag_value`` if given. """ @@ -2444,7 +2471,7 @@ class Option(Parameter): def __init__( self, param_decls: t.Optional[t.Sequence[str]] = None, - show_default: t.Union[bool, str] = False, + show_default: t.Union[bool, str, None] = None, prompt: t.Union[bool, str] = False, confirmation_prompt: t.Union[bool, str] = False, prompt_required: bool = True, @@ -2461,6 +2488,9 @@ def __init__( show_envvar: bool = False, **attrs: t.Any, ) -> None: + if help: + help = inspect.cleandoc(help) + default_is_missing = "default" not in attrs super().__init__(param_decls, type=type, multiple=multiple, **attrs) @@ -2472,7 +2502,7 @@ def __init__( elif prompt is False: prompt_text = None else: - prompt_text = t.cast(str, prompt) + prompt_text = prompt self.prompt = prompt_text self.confirmation_prompt = confirmation_prompt @@ -2499,7 +2529,7 @@ def __init__( # flag if flag_value is set. self._flag_needs_value = flag_value is not None - if is_flag and default_is_missing: + if is_flag and default_is_missing and not self.required: self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False if flag_value is None: @@ -2550,6 +2580,9 @@ def __init__( if self.is_flag: raise TypeError("'count' is not valid with 'is_flag'.") + if self.multiple and self.is_flag: + raise TypeError("'multiple' is not valid with 'is_flag', use 'count'.") + def to_info_dict(self) -> t.Dict[str, t.Any]: info_dict = super().to_info_dict() info_dict.update( @@ -2711,16 +2744,23 @@ def _write_opts(opts: t.Sequence[str]) -> str: finally: ctx.resilient_parsing = resilient - show_default_is_str = isinstance(self.show_default, str) + show_default = False + show_default_is_str = False - if show_default_is_str or ( - default_value is not None and (self.show_default or ctx.show_default) - ): + if self.show_default is not None: + if isinstance(self.show_default, str): + show_default_is_str = show_default = True + else: + show_default = self.show_default + elif ctx.show_default is not None: + show_default = ctx.show_default + + if show_default_is_str or (show_default and (default_value is not None)): if show_default_is_str: default_string = f"({self.show_default})" elif isinstance(default_value, (list, tuple)): default_string = ", ".join(str(d) for d in default_value) - elif callable(default_value): + elif inspect.isfunction(default_value): default_string = _("(dynamic)") elif self.is_bool_flag and self.secondary_opts: # For boolean flags that have distinct True/False opts, @@ -2728,6 +2768,8 @@ def _write_opts(opts: t.Sequence[str]) -> str: default_string = split_opt( (self.opts if self.default else self.secondary_opts)[0] )[1] + elif self.is_bool_flag and not self.secondary_opts and not default_value: + default_string = "" else: default_string = str(default_value) @@ -2753,13 +2795,13 @@ def _write_opts(opts: t.Sequence[str]) -> str: return ("; " if any_prefix_is_slash else " / ").join(rv), help - @typing.overload + @t.overload def get_default( self, ctx: Context, call: "te.Literal[True]" = True ) -> t.Optional[t.Any]: ... - @typing.overload + @t.overload def get_default( self, ctx: Context, call: bool = ... ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: @@ -2770,7 +2812,7 @@ def get_default( ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: # If we're a non boolean flag our default is more complex because # we need to look at all flags in the same group to figure out - # if we're the the default one in which case we return the flag + # if we're the default one in which case we return the flag # value as default. if self.is_flag and not self.is_bool_flag: for param in ctx.command.params: @@ -2821,7 +2863,10 @@ def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" rv = os.environ.get(envvar) - return rv + if rv: + return rv + + return None def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) diff --git a/pipenv/vendor/click/decorators.py b/pipenv/vendor/click/decorators.py index f1cc005af9..28618dc523 100644 --- a/pipenv/vendor/click/decorators.py +++ b/pipenv/vendor/click/decorators.py @@ -14,7 +14,7 @@ from .utils import echo F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -FC = t.TypeVar("FC", t.Callable[..., t.Any], Command) +FC = t.TypeVar("FC", bound=t.Union[t.Callable[..., t.Any], Command]) def pass_context(f: F) -> F: @@ -121,43 +121,38 @@ def new_func(*args, **kwargs): # type: ignore return decorator -def _make_command( - f: F, - name: t.Optional[str], - attrs: t.MutableMapping[str, t.Any], - cls: t.Type[Command], -) -> Command: - if isinstance(f, Command): - raise TypeError("Attempted to convert a callback into a command twice.") +CmdType = t.TypeVar("CmdType", bound=Command) - try: - params = f.__click_params__ # type: ignore - params.reverse() - del f.__click_params__ # type: ignore - except AttributeError: - params = [] - help = attrs.get("help") +@t.overload +def command( + __func: t.Callable[..., t.Any], +) -> Command: + ... + - if help is None: - help = inspect.getdoc(f) - else: - help = inspect.cleandoc(help) - - attrs["help"] = help - return cls( - name=name or f.__name__.lower().replace("_", "-"), - callback=f, - params=params, - **attrs, - ) +@t.overload +def command( + name: t.Optional[str] = None, + **attrs: t.Any, +) -> t.Callable[..., Command]: + ... +@t.overload def command( name: t.Optional[str] = None, + cls: t.Type[CmdType] = ..., + **attrs: t.Any, +) -> t.Callable[..., CmdType]: + ... + + +def command( + name: t.Union[str, t.Callable[..., t.Any], None] = None, cls: t.Optional[t.Type[Command]] = None, **attrs: t.Any, -) -> t.Callable[[F], Command]: +) -> t.Union[Command, t.Callable[..., Command]]: r"""Creates a new :class:`Command` and uses the decorated function as callback. This will also automatically attach all decorated :func:`option`\s and :func:`argument`\s as parameters to the command. @@ -167,6 +162,8 @@ def command( pass the intended name as the first argument. All keyword arguments are forwarded to the underlying command class. + For the ``params`` argument, any decorated params are appended to + the end of the list. Once decorated the function turns into a :class:`Command` instance that can be invoked as a command line utility or be attached to a @@ -176,24 +173,91 @@ def command( name with underscores replaced by dashes. :param cls: the command class to instantiate. This defaults to :class:`Command`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.1 + The ``params`` argument can be used. Decorated params are + appended to the end of the list. """ + + func: t.Optional[t.Callable[..., t.Any]] = None + + if callable(name): + func = name + name = None + assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class." + assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments." + if cls is None: cls = Command def decorator(f: t.Callable[..., t.Any]) -> Command: - cmd = _make_command(f, name, attrs, cls) # type: ignore + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + + attr_params = attrs.pop("params", None) + params = attr_params if attr_params is not None else [] + + try: + decorator_params = f.__click_params__ # type: ignore + except AttributeError: + pass + else: + del f.__click_params__ # type: ignore + params.extend(reversed(decorator_params)) + + if attrs.get("help") is None: + attrs["help"] = f.__doc__ + + cmd = cls( # type: ignore[misc] + name=name or f.__name__.lower().replace("_", "-"), # type: ignore[arg-type] + callback=f, + params=params, + **attrs, + ) cmd.__doc__ = f.__doc__ return cmd + if func is not None: + return decorator(func) + return decorator -def group(name: t.Optional[str] = None, **attrs: t.Any) -> t.Callable[[F], Group]: +@t.overload +def group( + __func: t.Callable[..., t.Any], +) -> Group: + ... + + +@t.overload +def group( + name: t.Optional[str] = None, + **attrs: t.Any, +) -> t.Callable[[F], Group]: + ... + + +def group( + name: t.Union[str, t.Callable[..., t.Any], None] = None, **attrs: t.Any +) -> t.Union[Group, t.Callable[[F], Group]]: """Creates a new :class:`Group` with a function as callback. This works otherwise the same as :func:`command` just that the `cls` parameter is set to :class:`Group`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. """ - attrs.setdefault("cls", Group) + if attrs.get("cls") is None: + attrs["cls"] = Group + + if callable(name): + grp: t.Callable[[F], Group] = t.cast(Group, command(**attrs)) + return grp(name) + return t.cast(Group, command(name, **attrs)) @@ -219,7 +283,7 @@ def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]: """ def decorator(f: FC) -> FC: - ArgumentClass = attrs.pop("cls", Argument) + ArgumentClass = attrs.pop("cls", None) or Argument _param_memo(f, ArgumentClass(param_decls, **attrs)) return f @@ -240,10 +304,7 @@ def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]: def decorator(f: FC) -> FC: # Issue 926, copy attrs, so pre-defined options can re-use the same cls= option_attrs = attrs.copy() - - if "help" in option_attrs: - option_attrs["help"] = inspect.cleandoc(option_attrs["help"]) - OptionClass = option_attrs.pop("cls", Option) + OptionClass = option_attrs.pop("cls", None) or Option _param_memo(f, OptionClass(param_decls, **option_attrs)) return f diff --git a/pipenv/vendor/click/globals.py b/pipenv/vendor/click/globals.py index a7b0c93171..480058f10d 100644 --- a/pipenv/vendor/click/globals.py +++ b/pipenv/vendor/click/globals.py @@ -1,4 +1,3 @@ -import typing import typing as t from threading import local @@ -9,12 +8,12 @@ _local = local() -@typing.overload +@t.overload def get_current_context(silent: "te.Literal[False]" = False) -> "Context": ... -@typing.overload +@t.overload def get_current_context(silent: bool = ...) -> t.Optional["Context"]: ... diff --git a/pipenv/vendor/click/shell_completion.py b/pipenv/vendor/click/shell_completion.py index cad080da6f..c17a8e643c 100644 --- a/pipenv/vendor/click/shell_completion.py +++ b/pipenv/vendor/click/shell_completion.py @@ -102,10 +102,10 @@ def __getattr__(self, name: str) -> t.Any: IFS=',' read type value <<< "$completion" if [[ $type == 'dir' ]]; then - COMREPLY=() + COMPREPLY=() compopt -o dirnames elif [[ $type == 'file' ]]; then - COMREPLY=() + COMPREPLY=() compopt -o default elif [[ $type == 'plain' ]]; then COMPREPLY+=($value) @@ -448,17 +448,16 @@ def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: ) -def _start_of_option(value: str) -> bool: +def _start_of_option(ctx: Context, value: str) -> bool: """Check if the value looks like the start of an option.""" if not value: return False c = value[0] - # Allow "/" since that starts a path. - return not c.isalnum() and c != "/" + return c in ctx._opt_prefixes -def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool: +def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) -> bool: """Determine if the given parameter is an option that needs a value. :param args: List of complete args before the incomplete value. @@ -467,7 +466,7 @@ def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool: if not isinstance(param, Option): return False - if param.is_flag: + if param.is_flag or param.count: return False last_option = None @@ -476,7 +475,7 @@ def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool: if index + 1 > param.nargs: break - if _start_of_option(arg): + if _start_of_option(ctx, arg): last_option = arg return last_option is not None and last_option in param.opts @@ -551,7 +550,7 @@ def _resolve_incomplete( # split and discard the "=" to make completion easier. if incomplete == "=": incomplete = "" - elif "=" in incomplete and _start_of_option(incomplete): + elif "=" in incomplete and _start_of_option(ctx, incomplete): name, _, incomplete = incomplete.partition("=") args.append(name) @@ -559,7 +558,7 @@ def _resolve_incomplete( # even if they start with the option character. If it hasn't been # given and the incomplete arg looks like an option, the current # command will provide option name completions. - if "--" not in args and _start_of_option(incomplete): + if "--" not in args and _start_of_option(ctx, incomplete): return ctx.command, incomplete params = ctx.command.get_params(ctx) @@ -567,7 +566,7 @@ def _resolve_incomplete( # If the last complete arg is an option name with an incomplete # value, the option will provide value completions. for param in params: - if _is_incomplete_option(args, param): + if _is_incomplete_option(ctx, args, param): return param, incomplete # It's not an option name or value. The first argument without a diff --git a/pipenv/vendor/click/termui.py b/pipenv/vendor/click/termui.py index cf8d5f132e..bfb2f5ae67 100644 --- a/pipenv/vendor/click/termui.py +++ b/pipenv/vendor/click/termui.py @@ -3,7 +3,6 @@ import itertools import os import sys -import typing import typing as t from gettext import gettext as _ @@ -94,7 +93,7 @@ def prompt( """Prompts a user for input. This is a convenience function that can be used to prompt a user for input later. - If the user aborts the input by sending a interrupt signal, this + If the user aborts the input by sending an interrupt signal, this function will catch it and raise a :exc:`Abort` exception. :param text: the text to show for the prompt. @@ -160,7 +159,6 @@ def prompt_func(text: str) -> str: if confirmation_prompt is True: confirmation_prompt = _("Repeat for confirmation") - confirmation_prompt = t.cast(str, confirmation_prompt) confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) while True: @@ -182,9 +180,9 @@ def prompt_func(text: str) -> str: if not confirmation_prompt: return result while True: - confirmation_prompt = t.cast(str, confirmation_prompt) value2 = prompt_func(confirmation_prompt) - if value2: + is_empty = not value and not value2 + if value2 or is_empty: break if value == value2: return result @@ -252,26 +250,6 @@ def confirm( return rv -def get_terminal_size() -> os.terminal_size: - """Returns the current size of the terminal as tuple in the form - ``(width, height)`` in columns and rows. - - .. deprecated:: 8.0 - Will be removed in Click 8.1. Use - :func:`shutil.get_terminal_size` instead. - """ - import shutil - import warnings - - warnings.warn( - "'click.get_terminal_size()' is deprecated and will be removed" - " in Click 8.1. Use 'shutil.get_terminal_size()' instead.", - DeprecationWarning, - stacklevel=2, - ) - return shutil.get_terminal_size() - - def echo_via_pager( text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str], color: t.Optional[bool] = None, @@ -627,7 +605,7 @@ def unstyle(text: str) -> str: def secho( message: t.Optional[t.Any] = None, - file: t.Optional[t.IO] = None, + file: t.Optional[t.IO[t.AnyStr]] = None, nl: bool = True, err: bool = False, color: t.Optional[bool] = None, diff --git a/pipenv/vendor/click/testing.py b/pipenv/vendor/click/testing.py index d19b850fc1..e395c2edfd 100644 --- a/pipenv/vendor/click/testing.py +++ b/pipenv/vendor/click/testing.py @@ -464,16 +464,16 @@ def isolated_filesystem( Added the ``temp_dir`` parameter. """ cwd = os.getcwd() - t = tempfile.mkdtemp(dir=temp_dir) - os.chdir(t) + dt = tempfile.mkdtemp(dir=temp_dir) # type: ignore[type-var] + os.chdir(dt) try: - yield t + yield t.cast(str, dt) finally: os.chdir(cwd) if temp_dir is None: try: - shutil.rmtree(t) + shutil.rmtree(dt) except OSError: # noqa: B014 pass diff --git a/pipenv/vendor/click/types.py b/pipenv/vendor/click/types.py index 5ad3d27b37..92e160366f 100644 --- a/pipenv/vendor/click/types.py +++ b/pipenv/vendor/click/types.py @@ -63,7 +63,14 @@ def to_info_dict(self) -> t.Dict[str, t.Any]: # The class name without the "ParamType" suffix. param_type = type(self).__name__.partition("ParamType")[0] param_type = param_type.partition("ParameterType")[0] - return {"param_type": param_type, "name": self.name} + + # Custom subclasses might not remember to set a name. + if hasattr(self, "name"): + name = self.name + else: + name = param_type + + return {"param_type": param_type, "name": name} def __call__( self, @@ -724,7 +731,7 @@ def convert( return f except OSError as e: # noqa: B014 - self.fail(f"{os.fsdecode(value)!r}: {e.strerror}", param, ctx) + self.fail(f"'{os.fsdecode(value)}': {e.strerror}", param, ctx) def shell_complete( self, ctx: "Context", param: "Parameter", incomplete: str @@ -744,30 +751,31 @@ def shell_complete( class Path(ParamType): - """The path type is similar to the :class:`File` type but it performs - different checks. First of all, instead of returning an open file - handle it returns just the filename. Secondly, it can perform various - basic checks about what the file or directory should be. - - :param exists: if set to true, the file or directory needs to exist for - this value to be valid. If this is not required and a - file does indeed not exist, then all further checks are - silently skipped. - :param file_okay: controls if a file is a possible value. - :param dir_okay: controls if a directory is a possible value. - :param writable: if true, a writable check is performed. + """The ``Path`` type is similar to the :class:`File` type, but + returns the filename instead of an open file. Various checks can be + enabled to validate the type of file and permissions. + + :param exists: The file or directory needs to exist for the value to + be valid. If this is not set to ``True``, and the file does not + exist, then all further checks are silently skipped. + :param file_okay: Allow a file as a value. + :param dir_okay: Allow a directory as a value. :param readable: if true, a readable check is performed. - :param resolve_path: if this is true, then the path is fully resolved - before the value is passed onwards. This means - that it's absolute and symlinks are resolved. It - will not expand a tilde-prefix, as this is - supposed to be done by the shell only. - :param allow_dash: If this is set to `True`, a single dash to indicate - standard streams is permitted. + :param writable: if true, a writable check is performed. + :param executable: if true, an executable check is performed. + :param resolve_path: Make the value absolute and resolve any + symlinks. A ``~`` is not expanded, as this is supposed to be + done by the shell only. + :param allow_dash: Allow a single dash as a value, which indicates + a standard stream (but does not open it). Use + :func:`~click.open_file` to handle opening this value. :param path_type: Convert the incoming path value to this type. If ``None``, keep Python's default, which is ``str``. Useful to convert to :class:`pathlib.Path`. + .. versionchanged:: 8.1 + Added the ``executable`` parameter. + .. versionchanged:: 8.0 Allow passing ``type=pathlib.Path``. @@ -787,12 +795,14 @@ def __init__( resolve_path: bool = False, allow_dash: bool = False, path_type: t.Optional[t.Type] = None, + executable: bool = False, ): self.exists = exists self.file_okay = file_okay self.dir_okay = dir_okay - self.writable = writable self.readable = readable + self.writable = writable + self.executable = executable self.resolve_path = resolve_path self.allow_dash = allow_dash self.type = path_type @@ -865,12 +875,22 @@ def convert( ) if not self.dir_okay and stat.S_ISDIR(st.st_mode): self.fail( - _("{name} {filename!r} is a directory.").format( + _("{name} '{filename}' is a directory.").format( name=self.name.title(), filename=os.fsdecode(value) ), param, ctx, ) + + if self.readable and not os.access(rv, os.R_OK): + self.fail( + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + if self.writable and not os.access(rv, os.W_OK): self.fail( _("{name} {filename!r} is not writable.").format( @@ -879,9 +899,10 @@ def convert( param, ctx, ) - if self.readable and not os.access(rv, os.R_OK): + + if self.executable and not os.access(value, os.X_OK): self.fail( - _("{name} {filename!r} is not readable.").format( + _("{name} {filename!r} is not executable.").format( name=self.name.title(), filename=os.fsdecode(value) ), param, diff --git a/pipenv/vendor/click/utils.py b/pipenv/vendor/click/utils.py index 16033d6235..8283788ace 100644 --- a/pipenv/vendor/click/utils.py +++ b/pipenv/vendor/click/utils.py @@ -1,4 +1,5 @@ import os +import re import sys import typing as t from functools import update_wrapper @@ -203,7 +204,7 @@ def __iter__(self) -> t.Iterator[t.AnyStr]: def echo( message: t.Optional[t.Any] = None, - file: t.Optional[t.IO] = None, + file: t.Optional[t.IO[t.Any]] = None, nl: bool = True, err: bool = False, color: t.Optional[bool] = None, @@ -340,53 +341,43 @@ def open_file( lazy: bool = False, atomic: bool = False, ) -> t.IO: - """This is similar to how the :class:`File` works but for manual - usage. Files are opened non lazy by default. This can open regular - files as well as stdin/stdout if ``'-'`` is passed. + """Open a file, with extra behavior to handle ``'-'`` to indicate + a standard stream, lazy open on write, and atomic write. Similar to + the behavior of the :class:`~click.File` param type. - If stdin/stdout is returned the stream is wrapped so that the context - manager will not close the stream accidentally. This makes it possible - to always use the function like this without having to worry to - accidentally close a standard stream:: + If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is + wrapped so that using it in a context manager will not close it. + This makes it possible to use the function without accidentally + closing a standard stream: + + .. code-block:: python with open_file(filename) as f: ... - .. versionadded:: 3.0 + :param filename: The name of the file to open, or ``'-'`` for + ``stdin``/``stdout``. + :param mode: The mode in which to open the file. + :param encoding: The encoding to decode or encode a file opened in + text mode. + :param errors: The error handling mode. + :param lazy: Wait to open the file until it is accessed. For read + mode, the file is temporarily opened to raise access errors + early, then closed until it is read again. + :param atomic: Write to a temporary file and replace the given file + on close. - :param filename: the name of the file to open (or ``'-'`` for stdin/stdout). - :param mode: the mode in which to open the file. - :param encoding: the encoding to use. - :param errors: the error handling for this file. - :param lazy: can be flipped to true to open the file lazily. - :param atomic: in atomic mode writes go into a temporary file and it's - moved on close. + .. versionadded:: 3.0 """ if lazy: return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic)) + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + if not should_close: f = t.cast(t.IO, KeepOpenFile(f)) - return f - - -def get_os_args() -> t.Sequence[str]: - """Returns the argument part of ``sys.argv``, removing the first - value which is the name of the script. - .. deprecated:: 8.0 - Will be removed in Click 8.1. Access ``sys.argv[1:]`` directly - instead. - """ - import warnings - - warnings.warn( - "'get_os_args' is deprecated and will be removed in Click 8.1." - " Access 'sys.argv[1:]' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - return sys.argv[1:] + return f def format_filename( @@ -484,7 +475,7 @@ def __getattr__(self, attr: str) -> t.Any: def _detect_program_name( - path: t.Optional[str] = None, _main: ModuleType = sys.modules["__main__"] + path: t.Optional[str] = None, _main: t.Optional[ModuleType] = None ) -> str: """Determine the command used to run the program, for use in help text. If a file or entry point was executed, the file name is @@ -506,6 +497,9 @@ def _detect_program_name( :meta private: """ + if _main is None: + _main = sys.modules["__main__"] + if not path: path = sys.argv[0] @@ -546,7 +540,7 @@ def _expand_args( See :func:`glob.glob`, :func:`os.path.expanduser`, and :func:`os.path.expandvars`. - This intended for use on Windows, where the shell does not do any + This is intended for use on Windows, where the shell does not do any expansion. It may not exactly match what a Unix shell would do. :param args: List of command line arguments to expand. @@ -554,6 +548,10 @@ def _expand_args( :param env: Expand environment variables. :param glob_recursive: ``**`` matches directories recursively. + .. versionchanged:: 8.1 + Invalid glob patterns are treated as empty expansions rather + than raising an error. + .. versionadded:: 8.0 :meta private: @@ -569,7 +567,10 @@ def _expand_args( if env: arg = os.path.expandvars(arg) - matches = glob(arg, recursive=glob_recursive) + try: + matches = glob(arg, recursive=glob_recursive) + except re.error: + matches = [] if not matches: out.append(arg) diff --git a/pipenv/vendor/click_didyoumean/__init__.py b/pipenv/vendor/click_didyoumean/__init__.py index c93b083bd9..8264e2e3fc 100644 --- a/pipenv/vendor/click_didyoumean/__init__.py +++ b/pipenv/vendor/click_didyoumean/__init__.py @@ -1,48 +1,56 @@ -# -*- coding: utf-8 -*- - """ - Extension for the python ``click`` module to provide - a group with a git-like *did-you-mean* feature. +Extension for ``click`` to provide a group +with a git-like *did-you-mean* feature. """ -import pipenv.vendor.click as click import difflib +import typing -__version__ = "0.0.3" +import pipenv.vendor.click as click -class DYMMixin(object): # pylint: disable=too-few-public-methods +class DYMMixin: """ Mixin class for click MultiCommand inherited classes to provide git-like *did-you-mean* functionality when a certain command is not registered. """ - def __init__(self, *args, **kwargs): + + def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: self.max_suggestions = kwargs.pop("max_suggestions", 3) self.cutoff = kwargs.pop("cutoff", 0.5) - super(DYMMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # type: ignore - def resolve_command(self, ctx, args): + def resolve_command( + self, ctx: click.Context, args: typing.List[str] + ) -> typing.Tuple[ + typing.Optional[str], typing.Optional[click.Command], typing.List[str] + ]: """ Overrides clicks ``resolve_command`` method and appends *Did you mean ...* suggestions to the raised exception message. """ - original_cmd_name = click.utils.make_str(args[0]) - try: - return super(DYMMixin, self).resolve_command(ctx, args) + return super(DYMMixin, self).resolve_command(ctx, args) # type: ignore except click.exceptions.UsageError as error: error_msg = str(error) - matches = difflib.get_close_matches(original_cmd_name, - self.list_commands(ctx), self.max_suggestions, self.cutoff) + original_cmd_name = click.utils.make_str(args[0]) + matches = difflib.get_close_matches( + original_cmd_name, + self.list_commands(ctx), # type: ignore + self.max_suggestions, + self.cutoff, + ) if matches: - error_msg += '\n\nDid you mean one of these?\n %s' % '\n '.join(matches) # pylint: disable=line-too-long + fmt_matches = "\n ".join(matches) + error_msg += "\n\n" + error_msg += f"Did you mean one of these?\n {fmt_matches}" raise click.exceptions.UsageError(error_msg, error.ctx) -class DYMGroup(DYMMixin, click.Group): # pylint: disable=too-many-public-methods +class DYMGroup(DYMMixin, click.Group): """ click Group to provide git-like *did-you-mean* functionality when a certain @@ -50,7 +58,7 @@ class DYMGroup(DYMMixin, click.Group): # pylint: disable=too-many-public-method """ -class DYMCommandCollection(DYMMixin, click.CommandCollection): # pylint: disable=too-many-public-methods +class DYMCommandCollection(DYMMixin, click.CommandCollection): """ click CommandCollection to provide git-like *did-you-mean* functionality when a certain diff --git a/pipenv/vendor/markupsafe/__init__.py b/pipenv/vendor/markupsafe/__init__.py index d331ac3622..7166b1920e 100644 --- a/pipenv/vendor/markupsafe/__init__.py +++ b/pipenv/vendor/markupsafe/__init__.py @@ -11,9 +11,10 @@ def __html__(self) -> str: pass -__version__ = "2.0.1" +__version__ = "2.1.2" -_striptags_re = re.compile(r"(|<[^>]*>)") +_strip_comments_re = re.compile(r"", re.DOTALL) +_strip_tags_re = re.compile(r"<.*?>", re.DOTALL) def _simple_escaping_wrapper(name: str) -> t.Callable[..., "Markup"]: @@ -92,19 +93,24 @@ def __radd__(self, other: t.Union[str, "HasHTML"]) -> "Markup": return NotImplemented - def __mul__(self, num: int) -> "Markup": + def __mul__(self, num: "te.SupportsIndex") -> "Markup": if isinstance(num, int): return self.__class__(super().__mul__(num)) - return NotImplemented # type: ignore + return NotImplemented __rmul__ = __mul__ def __mod__(self, arg: t.Any) -> "Markup": if isinstance(arg, tuple): + # a tuple of arguments, each wrapped arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg) - else: + elif hasattr(type(arg), "__getitem__") and not isinstance(arg, str): + # a mapping of arguments, wrapped arg = _MarkupEscapeHelper(arg, self.escape) + else: + # a single argument, wrapped with the helper and a tuple + arg = (_MarkupEscapeHelper(arg, self.escape),) return self.__class__(super().__mod__(arg)) @@ -153,8 +159,11 @@ def striptags(self) -> str: >>> Markup("Main »\tAbout").striptags() 'Main » About' """ - stripped = " ".join(_striptags_re.sub("", self).split()) - return Markup(stripped).unescape() + # Use two regexes to avoid ambiguous matches. + value = _strip_comments_re.sub("", self) + value = _strip_tags_re.sub("", value) + value = " ".join(value.split()) + return Markup(value).unescape() @classmethod def escape(cls, s: t.Any) -> "Markup": @@ -280,9 +289,7 @@ def __float__(self) -> float: from ._speedups import escape as escape from ._speedups import escape_silent as escape_silent from ._speedups import soft_str as soft_str - from ._speedups import soft_unicode except ImportError: from ._native import escape as escape from ._native import escape_silent as escape_silent # noqa: F401 from ._native import soft_str as soft_str # noqa: F401 - from ._native import soft_unicode # noqa: F401 diff --git a/pipenv/vendor/markupsafe/_native.py b/pipenv/vendor/markupsafe/_native.py index 6f7eb7a8cb..8117b2716d 100644 --- a/pipenv/vendor/markupsafe/_native.py +++ b/pipenv/vendor/markupsafe/_native.py @@ -61,15 +61,3 @@ def soft_str(s: t.Any) -> str: return str(s) return s - - -def soft_unicode(s: t.Any) -> str: - import warnings - - warnings.warn( - "'soft_unicode' has been renamed to 'soft_str'. The old name" - " will be removed in MarkupSafe 2.1.", - DeprecationWarning, - stacklevel=2, - ) - return soft_str(s) diff --git a/pipenv/vendor/pipdeptree/LICENSE b/pipenv/vendor/pipdeptree/LICENSE index 852f337a0f..d36eb27d34 100644 --- a/pipenv/vendor/pipdeptree/LICENSE +++ b/pipenv/vendor/pipdeptree/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 Vineet Naik (naikvin@gmail.com) +Copyright (c) The pipdeptree developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/pipenv/vendor/pipdeptree/__init__.py b/pipenv/vendor/pipdeptree/__init__.py index 069eabb154..475a2bb61e 100644 --- a/pipenv/vendor/pipdeptree/__init__.py +++ b/pipenv/vendor/pipdeptree/__init__.py @@ -1,4 +1,5 @@ import argparse +import fnmatch import inspect import json import os @@ -10,6 +11,7 @@ from collections.abc import Mapping from importlib import import_module from itertools import chain +from textwrap import dedent from pipenv.patched.pip._vendor import pkg_resources @@ -349,14 +351,14 @@ def filter(self, include, exclude): m = {} seen = set() for node in self._obj.keys(): - if node.key in exclude: + if any(fnmatch.fnmatch(node.key, e) for e in exclude): continue - if include is None or node.key in include: + if include is None or any(fnmatch.fnmatch(node.key, i) for i in include): stack.append(node) while True: if len(stack) > 0: n = stack.pop() - cldn = [c for c in self._obj[n] if c.key not in exclude] + cldn = [c for c in self._obj[n] if not any(fnmatch.fnmatch(c.key, e) for e in exclude)] m[n] = cldn seen.add(n.key) for c in cldn: @@ -550,6 +552,103 @@ def aux(node, parent=None, cur_chain=None): return json.dumps([aux(p) for p in nodes], indent=indent) +def render_mermaid(tree) -> str: + """Produce a Mermaid flowchart from the dependency graph. + + :param dict tree: dependency graph + """ + # List of reserved keywords in Mermaid that cannot be used as node names. + # See: https://github.com/mermaid-js/mermaid/issues/4182#issuecomment-1454787806 + reserved_ids: set[str] = { + "C4Component", + "C4Container", + "C4Deployment", + "C4Dynamic", + "_blank", + "_parent", + "_self", + "_top", + "call", + "class", + "classDef", + "click", + "end", + "flowchart", + "flowchart-v2", + "graph", + "interpolate", + "linkStyle", + "style", + "subgraph", + } + node_ids_map: dict[str:str] = {} + + def mermaid_id(key: str) -> str: + """Returns a valid Mermaid node ID from a string.""" + # If we have already seen this key, return the canonical ID. + canonical_id = node_ids_map.get(key) + if canonical_id is not None: + return canonical_id + # If the key is not a reserved keyword, return it as is, and update the map. + if key not in reserved_ids: + node_ids_map[key] = key + return key + # If the key is a reserved keyword, append a number to it. + number = 0 + while True: + new_id = f"{key}_{number}" + if new_id not in node_ids_map: + node_ids_map[key] = new_id + return new_id + number += 1 + + # Use a sets to avoid duplicate entries. + nodes: set[str] = set() + edges: set[str] = set() + + if isinstance(tree, ReversedPackageDAG): + for package, reverse_dependencies in tree.items(): + package_label = "\\n".join( + (package.project_name, "(missing)" if package.is_missing else package.installed_version) + ) + package_key = mermaid_id(package.key) + nodes.add(f'{package_key}["{package_label}"]') + for reverse_dependency in reverse_dependencies: + edge_label = reverse_dependency.req.version_spec or "any" + reverse_dependency_key = mermaid_id(reverse_dependency.key) + edges.add(f'{package_key} -- "{edge_label}" --> {reverse_dependency_key}') + else: + for package, dependencies in tree.items(): + package_label = "\\n".join((package.project_name, package.version)) + package_key = mermaid_id(package.key) + nodes.add(f'{package_key}["{package_label}"]') + for dependency in dependencies: + edge_label = dependency.version_spec or "any" + dependency_key = mermaid_id(dependency.key) + if dependency.is_missing: + dependency_label = f"{dependency.project_name}\\n(missing)" + nodes.add(f'{dependency_key}["{dependency_label}"]:::missing') + edges.add(f"{package_key} -.-> {dependency_key}") + else: + edges.add(f'{package_key} -- "{edge_label}" --> {dependency_key}') + + # Produce the Mermaid Markdown. + indent = " " * 4 + output = dedent( + f"""\ + flowchart TD + {indent}classDef missing stroke-dasharray: 5 + """ + ) + # Sort the nodes and edges to make the output deterministic. + output += indent + output += f"\n{indent}".join(node for node in sorted(nodes)) + output += "\n" + indent + output += f"\n{indent}".join(edge for edge in sorted(edges)) + output += "\n" + return output + + def dump_graphviz(tree, output_format="dot", is_reverse=False): """Output dependency graph as one of the supported GraphViz output formats. @@ -612,7 +711,11 @@ def dump_graphviz(tree, output_format="dot", is_reverse=False): # Allow output of dot format, even if GraphViz isn't installed. if output_format == "dot": - return graph.source + # Emulates graphviz.dot.Dot.__iter__() to force the sorting of graph.body. + # Fixes https://github.com/tox-dev/pipdeptree/issues/188 + # That way we can guarantee the output of the dot format is deterministic + # and stable. + return "".join([tuple(graph)[0]] + sorted(graph.body) + [graph._tail]) # As it's unknown if the selected output format is binary or not, try to # decode it as UTF8 and only print it out in binary if that's not possible. @@ -742,12 +845,20 @@ def get_parser(): parser.add_argument( "-p", "--packages", - help="Comma separated list of select packages to show " "in the output. If set, --all will be ignored.", + help=( + "Comma separated list of select packages to show in the output. " + "Wildcards are supported, like 'somepackage.*'. " + "If set, --all will be ignored." + ), ) parser.add_argument( "-e", "--exclude", - help="Comma separated list of select packages to exclude " "from the output. If set, --all will be ignored.", + help=( + "Comma separated list of select packages to exclude from the output. " + "Wildcards are supported, like 'somepackage.*'. " + "If set, --all will be ignored." + ), metavar="PACKAGES", ) parser.add_argument( @@ -771,6 +882,12 @@ def get_parser(): "This option overrides all other options (except --json)." ), ) + parser.add_argument( + "--mermaid", + action="store_true", + default=False, + help=("Display dependency tree as a Maermaid graph. " "This option overrides all other options."), + ) parser.add_argument( "--graph-output", dest="output_format", @@ -880,6 +997,8 @@ def main(): print(render_json(tree, indent=4)) elif args.json_tree: print(render_json_tree(tree, indent=4)) + elif args.mermaid: + print(render_mermaid(tree)) elif args.output_format: output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse) print_graphviz(output) diff --git a/pipenv/vendor/pipdeptree/version.py b/pipenv/vendor/pipdeptree/version.py index 26d6fae0ee..693ca4c6f7 100644 --- a/pipenv/vendor/pipdeptree/version.py +++ b/pipenv/vendor/pipdeptree/version.py @@ -1,5 +1,4 @@ -# coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control -__version__ = version = '2.3.1' -__version_tuple__ = version_tuple = (2, 3, 1) +__version__ = version = '2.7.0' +__version_tuple__ = version_tuple = (2, 7, 0) diff --git a/pipenv/vendor/shellingham/__init__.py b/pipenv/vendor/shellingham/__init__.py index 0ce2e193b6..2c079d563f 100644 --- a/pipenv/vendor/shellingham/__init__.py +++ b/pipenv/vendor/shellingham/__init__.py @@ -4,7 +4,7 @@ from ._core import ShellDetectionFailure -__version__ = "1.5.0" +__version__ = "1.5.0.post1" def detect_shell(pid=None, max_depth=10): diff --git a/pipenv/vendor/tomlkit/__init__.py b/pipenv/vendor/tomlkit/__init__.py index 84566d4539..da9c48b4a9 100644 --- a/pipenv/vendor/tomlkit/__init__.py +++ b/pipenv/vendor/tomlkit/__init__.py @@ -1,31 +1,31 @@ -from .api import TOMLDocument -from .api import aot -from .api import array -from .api import boolean -from .api import comment -from .api import date -from .api import datetime -from .api import document -from .api import dump -from .api import dumps -from .api import float_ -from .api import inline_table -from .api import integer -from .api import item -from .api import key -from .api import key_value -from .api import load -from .api import loads -from .api import nl -from .api import parse -from .api import string -from .api import table -from .api import time -from .api import value -from .api import ws +from pipenv.vendor.tomlkit.api import TOMLDocument +from pipenv.vendor.tomlkit.api import aot +from pipenv.vendor.tomlkit.api import array +from pipenv.vendor.tomlkit.api import boolean +from pipenv.vendor.tomlkit.api import comment +from pipenv.vendor.tomlkit.api import date +from pipenv.vendor.tomlkit.api import datetime +from pipenv.vendor.tomlkit.api import document +from pipenv.vendor.tomlkit.api import dump +from pipenv.vendor.tomlkit.api import dumps +from pipenv.vendor.tomlkit.api import float_ +from pipenv.vendor.tomlkit.api import inline_table +from pipenv.vendor.tomlkit.api import integer +from pipenv.vendor.tomlkit.api import item +from pipenv.vendor.tomlkit.api import key +from pipenv.vendor.tomlkit.api import key_value +from pipenv.vendor.tomlkit.api import load +from pipenv.vendor.tomlkit.api import loads +from pipenv.vendor.tomlkit.api import nl +from pipenv.vendor.tomlkit.api import parse +from pipenv.vendor.tomlkit.api import string +from pipenv.vendor.tomlkit.api import table +from pipenv.vendor.tomlkit.api import time +from pipenv.vendor.tomlkit.api import value +from pipenv.vendor.tomlkit.api import ws -__version__ = "0.9.2" +__version__ = "0.11.7" __all__ = [ "aot", "array", @@ -49,6 +49,7 @@ "string", "table", "time", + "TOMLDocument", "value", "ws", ] diff --git a/pipenv/vendor/tomlkit/_compat.py b/pipenv/vendor/tomlkit/_compat.py index 1295df6990..f1d3bccd66 100644 --- a/pipenv/vendor/tomlkit/_compat.py +++ b/pipenv/vendor/tomlkit/_compat.py @@ -1,3 +1,4 @@ +import contextlib import sys from typing import Any @@ -15,9 +16,7 @@ def decode(string: Any, encodings: Optional[List[str]] = None): encodings = encodings or ["utf-8", "latin1", "ascii"] for encoding in encodings: - try: + with contextlib.suppress(UnicodeEncodeError, UnicodeDecodeError): return string.decode(encoding) - except (UnicodeEncodeError, UnicodeDecodeError): - pass return string.decode(encodings[0], errors="ignore") diff --git a/pipenv/vendor/tomlkit/_utils.py b/pipenv/vendor/tomlkit/_utils.py index f3fa49ff9a..52a7d3ee21 100644 --- a/pipenv/vendor/tomlkit/_utils.py +++ b/pipenv/vendor/tomlkit/_utils.py @@ -6,9 +6,10 @@ from datetime import time from datetime import timedelta from datetime import timezone +from typing import Collection from typing import Union -from ._compat import decode +from pipenv.vendor.tomlkit._compat import decode RFC_3339_LOOSE = re.compile( @@ -97,31 +98,48 @@ def parse_rfc3339(string: str) -> Union[datetime, date, time]: raise ValueError("Invalid RFC 339 string") -_escaped = {"b": "\b", "t": "\t", "n": "\n", "f": "\f", "r": "\r", '"': '"', "\\": "\\"} -_escapes = {v: k for k, v in _escaped.items()} +# https://toml.io/en/v1.0.0#string +CONTROL_CHARS = frozenset(chr(c) for c in range(0x20)) | {chr(0x7F)} +_escaped = { + "b": "\b", + "t": "\t", + "n": "\n", + "f": "\f", + "r": "\r", + '"': '"', + "\\": "\\", +} +_compact_escapes = { + **{v: f"\\{k}" for k, v in _escaped.items()}, + '"""': '""\\"', +} +_basic_escapes = CONTROL_CHARS | {'"', "\\"} -def escape_string(s: str) -> str: +def _unicode_escape(seq: str) -> str: + return "".join(f"\\u{ord(c):04x}" for c in seq) + + +def escape_string(s: str, escape_sequences: Collection[str] = _basic_escapes) -> str: s = decode(s) res = [] start = 0 - def flush(): + def flush(inc=1): if start != i: res.append(s[start:i]) - return i + 1 + return i + inc i = 0 while i < len(s): - c = s[i] - if c in '"\\\n\r\t\b\f': - start = flush() - res.append("\\" + _escapes[c]) - elif ord(c) < 0x20: - start = flush() - res.append("\\u%04x" % ord(c)) + for seq in escape_sequences: + seq_len = len(seq) + if s[i:].startswith(seq): + start = flush(seq_len) + res.append(_compact_escapes.get(seq) or _unicode_escape(seq)) + i += seq_len - 1 # fast-forward escape sequence i += 1 flush() diff --git a/pipenv/vendor/tomlkit/api.py b/pipenv/vendor/tomlkit/api.py index c4cc07ff22..84575ace2e 100644 --- a/pipenv/vendor/tomlkit/api.py +++ b/pipenv/vendor/tomlkit/api.py @@ -3,33 +3,35 @@ from collections.abc import Mapping from typing import IO from typing import Iterable +from typing import Optional from typing import Tuple from typing import Union -from ._utils import parse_rfc3339 -from .container import Container -from .exceptions import UnexpectedCharError -from .items import AoT -from .items import Array -from .items import Bool -from .items import Comment -from .items import Date -from .items import DateTime -from .items import DottedKey -from .items import Float -from .items import InlineTable -from .items import Integer -from .items import Item as _Item -from .items import Key -from .items import SingleKey -from .items import String -from .items import Table -from .items import Time -from .items import Trivia -from .items import Whitespace -from .items import item -from .parser import Parser -from .toml_document import TOMLDocument +from pipenv.vendor.tomlkit._utils import parse_rfc3339 +from pipenv.vendor.tomlkit.container import Container +from pipenv.vendor.tomlkit.exceptions import UnexpectedCharError +from pipenv.vendor.tomlkit.items import AoT +from pipenv.vendor.tomlkit.items import Array +from pipenv.vendor.tomlkit.items import Bool +from pipenv.vendor.tomlkit.items import Comment +from pipenv.vendor.tomlkit.items import Date +from pipenv.vendor.tomlkit.items import DateTime +from pipenv.vendor.tomlkit.items import DottedKey +from pipenv.vendor.tomlkit.items import Float +from pipenv.vendor.tomlkit.items import InlineTable +from pipenv.vendor.tomlkit.items import Integer +from pipenv.vendor.tomlkit.items import Item as _Item +from pipenv.vendor.tomlkit.items import Key +from pipenv.vendor.tomlkit.items import SingleKey +from pipenv.vendor.tomlkit.items import String +from pipenv.vendor.tomlkit.items import StringType as _StringType +from pipenv.vendor.tomlkit.items import Table +from pipenv.vendor.tomlkit.items import Time +from pipenv.vendor.tomlkit.items import Trivia +from pipenv.vendor.tomlkit.items import Whitespace +from pipenv.vendor.tomlkit.items import item +from pipenv.vendor.tomlkit.parser import Parser +from pipenv.vendor.tomlkit.toml_document import TOMLDocument def loads(string: Union[str, bytes]) -> TOMLDocument: @@ -57,7 +59,7 @@ def dumps(data: Mapping, sort_keys: bool = False) -> str: raise TypeError(msg) from ex -def load(fp: IO) -> TOMLDocument: +def load(fp: Union[IO[str], IO[bytes]]) -> TOMLDocument: """ Load toml document from a file-like object. """ @@ -104,9 +106,28 @@ def boolean(raw: str) -> Bool: return item(raw == "true") -def string(raw: str) -> String: - """Create a string item.""" - return item(raw) +def string( + raw: str, + *, + literal: bool = False, + multiline: bool = False, + escape: bool = True, +) -> String: + """Create a string item. + + By default, this function will create *single line basic* strings, but + boolean flags (e.g. ``literal=True`` and/or ``multiline=True``) + can be used for personalization. + + For more information, please check the spec: ``__. + + Common escaping rules will be applied for basic strings. + This can be controlled by explicitly setting ``escape=False``. + Please note that, if you disable escaping, you will have to make sure that + the given strings don't contain any forbidden character or sequence. + """ + type_ = _StringType.select(literal, multiline) + return String.from_raw(raw, type_, escape) def date(raw: str) -> Date: @@ -154,7 +175,7 @@ def array(raw: str = None) -> Array: return value(raw) -def table(is_super_table: bool = False) -> Table: +def table(is_super_table: Optional[bool] = None) -> Table: """Create an empty table. :param is_super_table: if true, the table is a super table diff --git a/pipenv/vendor/tomlkit/container.py b/pipenv/vendor/tomlkit/container.py index c20a6ab39a..ef1caef34e 100644 --- a/pipenv/vendor/tomlkit/container.py +++ b/pipenv/vendor/tomlkit/container.py @@ -8,22 +8,22 @@ from typing import Tuple from typing import Union -from ._compat import decode -from ._utils import merge_dicts -from .exceptions import KeyAlreadyPresent -from .exceptions import NonExistentKey -from .exceptions import TOMLKitError -from .items import AoT -from .items import Comment -from .items import Item -from .items import Key -from .items import Null -from .items import SingleKey -from .items import Table -from .items import Trivia -from .items import Whitespace -from .items import _CustomDict -from .items import item as _item +from pipenv.vendor.tomlkit._compat import decode +from pipenv.vendor.tomlkit._utils import merge_dicts +from pipenv.vendor.tomlkit.exceptions import KeyAlreadyPresent +from pipenv.vendor.tomlkit.exceptions import NonExistentKey +from pipenv.vendor.tomlkit.exceptions import TOMLKitError +from pipenv.vendor.tomlkit.items import AoT +from pipenv.vendor.tomlkit.items import Comment +from pipenv.vendor.tomlkit.items import Item +from pipenv.vendor.tomlkit.items import Key +from pipenv.vendor.tomlkit.items import Null +from pipenv.vendor.tomlkit.items import SingleKey +from pipenv.vendor.tomlkit.items import Table +from pipenv.vendor.tomlkit.items import Trivia +from pipenv.vendor.tomlkit.items import Whitespace +from pipenv.vendor.tomlkit.items import _CustomDict +from pipenv.vendor.tomlkit.items import item as _item _NOT_SET = object() @@ -46,8 +46,27 @@ def __init__(self, parsed: bool = False) -> None: def body(self) -> List[Tuple[Optional[Key], Item]]: return self._body + def unwrap(self) -> Dict[str, Any]: + unwrapped = {} + for k, v in self.items(): + if k is None: + continue + + if isinstance(k, Key): + k = k.key + + if hasattr(v, "unwrap"): + v = v.unwrap() + + if k in unwrapped: + merge_dicts(unwrapped[k], v) + else: + unwrapped[k] = v + + return unwrapped + @property - def value(self) -> Dict[Any, Any]: + def value(self) -> Dict[str, Any]: d = {} for k, v in self._body: if k is None: @@ -173,9 +192,9 @@ def append(self, key: Union[Key, str, None], item: Item) -> "Container": item.name = key.key prev = self._previous_item() - prev_ws = isinstance(prev, Whitespace) or ends_with_withespace(prev) + prev_ws = isinstance(prev, Whitespace) or ends_with_whitespace(prev) if isinstance(item, Table): - if item.name != key.key: + if not self._parsed: item.invalidate_display_name() if self._body and not (self._parsed or item.trivia.indent or prev_ws): item.trivia.indent = "\n" @@ -291,7 +310,7 @@ def append(self, key: Union[Key, str, None], item: Item) -> "Container": previous_item = self._body[-1][1] if not ( isinstance(previous_item, Whitespace) - or ends_with_withespace(previous_item) + or ends_with_whitespace(previous_item) or is_table or "\n" in previous_item.trivia.trail ): @@ -327,6 +346,25 @@ def append(self, key: Union[Key, str, None], item: Item) -> "Container": return self + def _remove_at(self, idx: int) -> None: + key = self._body[idx][0] + index = self._map.get(key) + if index is None: + raise NonExistentKey(key) + self._body[idx] = (None, Null()) + + if isinstance(index, tuple): + index = list(index) + index.remove(idx) + if len(index) == 1: + index = index.pop() + else: + index = tuple(index) + self._map[key] = index + else: + dict.__delitem__(self, key.key) + self._map.pop(key) + def remove(self, key: Union[Key, str]) -> "Container": """Remove a key from the container.""" if not isinstance(key, Key): @@ -406,7 +444,7 @@ def _insert_at(self, idx: int, key: Union[Key, str], item: Any) -> "Container": previous_item = self._body[idx - 1][1] if not ( isinstance(previous_item, Whitespace) - or ends_with_withespace(previous_item) + or ends_with_whitespace(previous_item) or isinstance(item, (AoT, Table)) or "\n" in previous_item.trivia.trail ): @@ -487,7 +525,8 @@ def _render_table( if not table.is_super_table() or ( any( - not isinstance(v, (Table, AoT, Whitespace)) for _, v in table.value.body + not isinstance(v, (Table, AoT, Whitespace, Null)) + for _, v in table.value.body ) and not key.is_dotted() ): @@ -495,16 +534,21 @@ def _render_table( if table.is_aot_element(): open_, close = "[[", "]]" - cur += "{}{}{}{}{}{}{}{}".format( - table.trivia.indent, - open_, - decode(_key), - close, - table.trivia.comment_ws, - decode(table.trivia.comment), - table.trivia.trail, - "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else "", + newline_in_table_trivia = ( + "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else "" + ) + cur += ( + f"{table.trivia.indent}" + f"{open_}" + f"{decode(_key)}" + f"{close}" + f"{table.trivia.comment_ws}" + f"{decode(table.trivia.comment)}" + f"{table.trivia.trail}" + f"{newline_in_table_trivia}" ) + elif table.trivia.indent == "\n": + cur += table.trivia.indent for k, v in table.value.body: if isinstance(v, Table): @@ -545,14 +589,14 @@ def _render_aot_table(self, table: Table, prefix: Optional[str] = None) -> str: if not table.is_super_table(): open_, close = "[[", "]]" - cur += "{}{}{}{}{}{}{}".format( - table.trivia.indent, - open_, - decode(_key), - close, - table.trivia.comment_ws, - decode(table.trivia.comment), - table.trivia.trail, + cur += ( + f"{table.trivia.indent}" + f"{open_}" + f"{decode(_key)}" + f"{close}" + f"{table.trivia.comment_ws}" + f"{decode(table.trivia.comment)}" + f"{table.trivia.trail}" ) for k, v in table.value.body: @@ -580,14 +624,14 @@ def _render_simple_item(self, key, item, prefix=None): if prefix is not None: _key = prefix + "." + _key - return "{}{}{}{}{}{}{}".format( - item.trivia.indent, - decode(_key), - key.sep, - decode(item.as_string()), - item.trivia.comment_ws, - decode(item.trivia.comment), - item.trivia.trail, + return ( + f"{item.trivia.indent}" + f"{decode(_key)}" + f"{key.sep}" + f"{decode(item.as_string())}" + f"{item.trivia.comment_ws}" + f"{decode(item.trivia.comment)}" + f"{item.trivia.trail}" ) def __len__(self) -> int: @@ -698,7 +742,7 @@ def _replace_at( # - it is not the last item last, _ = self._previous_item_with_index() idx = last if idx < 0 else idx - has_ws = ends_with_withespace(value) + has_ws = ends_with_whitespace(value) next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace) if idx < last and not (next_ws or has_ws): value.append(None, Whitespace("\n")) @@ -783,7 +827,7 @@ def __init__(self, container: Container, indices: Tuple[int]) -> None: self._tables_map = {} for i in indices: - key, item = self._container._body[i] + _, item = self._container._body[i] if isinstance(item, Table): self._tables.append(item) @@ -794,6 +838,9 @@ def __init__(self, container: Container, indices: Tuple[int]) -> None: if k is not None: dict.__setitem__(self, k.key, v) + def unwrap(self) -> str: + return self._internal_container.unwrap() + @property def value(self): return self._internal_container.value @@ -818,10 +865,20 @@ def __setitem__(self, key: Union[Key, str], item: Any) -> None: if key is not None: dict.__setitem__(self, key, item) + def _remove_table(self, table: Table) -> None: + """Remove table from the parent container""" + self._tables.remove(table) + for idx, item in enumerate(self._container._body): + if item[1] is table: + self._container._remove_at(idx) + break + def __delitem__(self, key: Union[Key, str]) -> None: if key in self._tables_map: table = self._tables[self._tables_map[key]] del table[key] + if not table and len(self._tables) > 1: + self._remove_table(table) del self._tables_map[key] else: raise NonExistentKey(key) @@ -836,15 +893,12 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return dict.__len__(self) - def __getattr__(self, attribute): - return getattr(self._internal_container, attribute) - def setdefault(self, key: Union[Key, str], default: Any) -> Any: super().setdefault(key, default=default) return self[key] -def ends_with_withespace(it: Any) -> bool: +def ends_with_whitespace(it: Any) -> bool: """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object ending with a ``Whitespace``. """ diff --git a/pipenv/vendor/tomlkit/exceptions.py b/pipenv/vendor/tomlkit/exceptions.py index 66370dbccf..3147ca2a22 100644 --- a/pipenv/vendor/tomlkit/exceptions.py +++ b/pipenv/vendor/tomlkit/exceptions.py @@ -1,3 +1,4 @@ +from typing import Collection from typing import Optional @@ -193,6 +194,7 @@ class KeyAlreadyPresent(TOMLKitError): """ def __init__(self, key): + key = getattr(key, "key", key) message = f'Key "{key}" already exists.' super().__init__(message) @@ -208,8 +210,18 @@ def __init__(self, line: int, col: int, char: int, type: str) -> None: display_code += hex(char)[2:] message = ( - "Control characters (codes less than 0x1f and 0x7f) are not allowed in {}, " - "use {} instead".format(type, display_code) + "Control characters (codes less than 0x1f and 0x7f)" + f" are not allowed in {type}, " + f"use {display_code} instead" ) super().__init__(line, col, message=message) + + +class InvalidStringError(ValueError, TOMLKitError): + def __init__(self, value: str, invalid_sequences: Collection[str], delimiter: str): + repr_ = repr(value)[1:-1] + super().__init__( + f"Invalid string: {delimiter}{repr_}{delimiter}. " + f"The character sequences {invalid_sequences} are invalid." + ) diff --git a/pipenv/vendor/tomlkit/items.py b/pipenv/vendor/tomlkit/items.py index abbb05fd81..c63ac7860a 100644 --- a/pipenv/vendor/tomlkit/items.py +++ b/pipenv/vendor/tomlkit/items.py @@ -8,23 +8,25 @@ from datetime import time from datetime import tzinfo from enum import Enum -from functools import lru_cache from typing import TYPE_CHECKING from typing import Any +from typing import Collection from typing import Dict from typing import Iterable from typing import Iterator from typing import List from typing import Optional +from typing import Sequence from typing import TypeVar from typing import Union from typing import cast from typing import overload -from ._compat import PY38 -from ._compat import decode -from ._utils import escape_string -from .toml_char import TOMLChar +from pipenv.vendor.tomlkit._compat import PY38 +from pipenv.vendor.tomlkit._compat import decode +from pipenv.vendor.tomlkit._utils import CONTROL_CHARS +from pipenv.vendor.tomlkit._utils import escape_string +from pipenv.vendor.tomlkit.exceptions import InvalidStringError if TYPE_CHECKING: # pragma: no cover @@ -39,11 +41,11 @@ # Importing from builtins is preferred over simple assignment, see issues: # https://github.com/python/mypy/issues/8715 # https://github.com/python/mypy/issues/10068 - from builtins import dict as _CustomDict - from builtins import list as _CustomList + from builtins import dict as _CustomDict # noqa: N812, TC004 + from builtins import list as _CustomList # noqa: N812, TC004 # Allow type annotations but break circular imports - from . import container + from pipenv.vendor.tomlkit import container else: from collections.abc import MutableMapping from collections.abc import MutableSequence @@ -55,6 +57,91 @@ class _CustomDict(MutableMapping, dict): """Adds MutableMapping mixin while pretending to be a builtin dict""" +ItemT = TypeVar("ItemT", bound="Item") + + +@overload +def item( + value: bool, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "Bool": + ... + + +@overload +def item( + value: int, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "Integer": + ... + + +@overload +def item( + value: float, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "Float": + ... + + +@overload +def item( + value: str, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "String": + ... + + +@overload +def item( + value: datetime, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "DateTime": + ... + + +@overload +def item( + value: date, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "Date": + ... + + +@overload +def item( + value: time, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "Time": + ... + + +@overload +def item( + value: Sequence[dict], _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "AoT": + ... + + +@overload +def item( + value: Sequence, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "Array": + ... + + +@overload +def item(value: dict, _parent: "Array" = ..., _sort_keys: bool = ...) -> "InlineTable": + ... + + +@overload +def item( + value: dict, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> "Table": + ... + + +@overload +def item( + value: ItemT, _parent: Optional["Item"] = ..., _sort_keys: bool = ... +) -> ItemT: + ... + + def item( value: Any, _parent: Optional["Item"] = None, _sort_keys: bool = False ) -> "Item": @@ -71,7 +158,7 @@ def item( b = 2 """ - from .container import Container + from pipenv.vendor.tomlkit.container import Container if isinstance(value, Item): return value @@ -83,25 +170,23 @@ def item( elif isinstance(value, float): return Float(value, Trivia(), str(value)) elif isinstance(value, dict): - table_constructor = InlineTable if isinstance(_parent, Array) else Table + table_constructor = ( + InlineTable if isinstance(_parent, (Array, InlineTable)) else Table + ) val = table_constructor(Container(), Trivia(), False) for k, v in sorted( value.items(), key=lambda i: (isinstance(i[1], dict), i[0] if _sort_keys else 1), ): val[k] = item(v, _parent=val, _sort_keys=_sort_keys) - only_child = len(value) == 1 and val[next(iter(value))] - if ( - table_constructor is Table - and only_child - and isinstance(only_child, (AoT, Table)) - ): - # The table becomes super table if the only child is a table or AoT. - val._is_super_table = True return val elif isinstance(value, (list, tuple)): - if value and all(isinstance(v, dict) for v in value): + if ( + value + and all(isinstance(v, dict) for v in value) + and (_parent is None or isinstance(_parent, Table)) + ): a = AoT([]) table_constructor = Table else: @@ -116,7 +201,7 @@ def item( v.items(), key=lambda i: (isinstance(i[1], dict), i[0] if _sort_keys else 1), ): - i = item(_v, _parent=a, _sort_keys=_sort_keys) + i = item(_v, _parent=table, _sort_keys=_sort_keys) if isinstance(table, InlineTable): i.trivia.trail = "" @@ -128,9 +213,7 @@ def item( return a elif isinstance(value, str): - escaped = escape_string(value) - - return String(StringType.SLB, decode(value), escaped, Trivia()) + return String.from_raw(value) elif isinstance(value, datetime): return DateTime( value.year, @@ -170,28 +253,55 @@ class StringType(Enum): # Multi Line Literal MLL = "'''" + @classmethod + def select(cls, literal=False, multiline=False) -> "StringType": + return { + (False, False): cls.SLB, + (False, True): cls.MLB, + (True, False): cls.SLL, + (True, True): cls.MLL, + }[(literal, multiline)] + + @property + def escaped_sequences(self) -> Collection[str]: + # https://toml.io/en/v1.0.0#string + escaped_in_basic = CONTROL_CHARS | {"\\"} + allowed_in_multiline = {"\n", "\r"} + return { + StringType.SLB: escaped_in_basic | {'"'}, + StringType.MLB: (escaped_in_basic | {'"""'}) - allowed_in_multiline, + StringType.SLL: (), + StringType.MLL: (), + }[self] + + @property + def invalid_sequences(self) -> Collection[str]: + # https://toml.io/en/v1.0.0#string + forbidden_in_literal = CONTROL_CHARS - {"\t"} + allowed_in_multiline = {"\n", "\r"} + return { + StringType.SLB: (), + StringType.MLB: (), + StringType.SLL: forbidden_in_literal | {"'"}, + StringType.MLL: (forbidden_in_literal | {"'''"}) - allowed_in_multiline, + }[self] + @property - @lru_cache(maxsize=None) def unit(self) -> str: return self.value[0] - @lru_cache(maxsize=None) def is_basic(self) -> bool: return self in {StringType.SLB, StringType.MLB} - @lru_cache(maxsize=None) def is_literal(self) -> bool: return self in {StringType.SLL, StringType.MLL} - @lru_cache(maxsize=None) def is_singleline(self) -> bool: return self in {StringType.SLB, StringType.SLL} - @lru_cache(maxsize=None) def is_multiline(self) -> bool: return self in {StringType.MLB, StringType.MLL} - @lru_cache(maxsize=None) def toggle(self) -> "StringType": return { StringType.SLB: StringType.MLB, @@ -205,7 +315,6 @@ class BoolType(Enum): TRUE = "true" FALSE = "false" - @lru_cache(maxsize=None) def __bool__(self): return {BoolType.TRUE: True, BoolType.FALSE: False}[self] @@ -396,6 +505,14 @@ def as_string(self) -> str: """The TOML representation""" raise NotImplementedError() + @property + def value(self) -> Any: + return self + + def unwrap(self) -> Any: + """Returns as pure python object (ppo)""" + raise NotImplementedError() + # Helpers def comment(self, comment: str) -> "Item": @@ -489,8 +606,8 @@ def discriminant(self) -> int: return 1 def as_string(self) -> str: - return "{}{}{}".format( - self._trivia.indent, decode(self._trivia.comment), self._trivia.trail + return ( + f"{self._trivia.indent}{decode(self._trivia.comment)}{self._trivia.trail}" ) def __str__(self) -> str: @@ -514,6 +631,9 @@ def __init__(self, _: int, trivia: Trivia, raw: str) -> None: if re.match(r"^[+\-]\d+$", raw): self._sign = True + def unwrap(self) -> int: + return int(self) + @property def discriminant(self) -> int: return 2 @@ -528,33 +648,30 @@ def as_string(self) -> str: def __add__(self, other): result = super().__add__(other) - + if result is NotImplemented: + return result return self._new(result) def __radd__(self, other): result = super().__radd__(other) - - if isinstance(other, Integer): - return self._new(result) - - return result + if result is NotImplemented: + return result + return self._new(result) def __sub__(self, other): result = super().__sub__(other) - + if result is NotImplemented: + return result return self._new(result) def __rsub__(self, other): result = super().__rsub__(other) - - if isinstance(other, Integer): - return self._new(result) - - return result + if result is NotImplemented: + return result + return self._new(result) def _new(self, result): raw = str(result) - if self._sign: sign = "+" if result >= 0 else "-" raw = sign + raw @@ -582,6 +699,9 @@ def __init__(self, _: float, trivia: Trivia, raw: str) -> None: if re.match(r"^[+\-].+$", raw): self._sign = True + def unwrap(self) -> float: + return float(self) + @property def discriminant(self) -> int: return 3 @@ -643,6 +763,9 @@ def __init__(self, t: int, trivia: Trivia) -> None: self._value = bool(t) + def unwrap(self) -> bool: + return bool(self) + @property def discriminant(self) -> int: return 4 @@ -691,8 +814,7 @@ def __new__( second: int, microsecond: int, tzinfo: Optional[tzinfo], - trivia: Trivia, - raw: str, + *_: Any, **kwargs: Any, ) -> datetime: return datetime.__new__( @@ -718,12 +840,28 @@ def __init__( second: int, microsecond: int, tzinfo: Optional[tzinfo], - trivia: Trivia, - raw: str, + trivia: Optional[Trivia] = None, + raw: Optional[str] = None, + **kwargs: Any, ) -> None: - super().__init__(trivia) + super().__init__(trivia or Trivia()) - self._raw = raw + self._raw = raw or self.isoformat() + + def unwrap(self) -> datetime: + ( + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo, + _, + _, + ) = self._getstate() + return datetime(year, month, day, hour, minute, second, microsecond, tzinfo) @property def discriminant(self) -> int: @@ -773,7 +911,16 @@ def __sub__(self, other): return result - def _new(self, result): + def replace(self, *args: Any, **kwargs: Any) -> datetime: + return self._new(super().replace(*args, **kwargs)) + + def astimezone(self, tz: tzinfo) -> datetime: + result = super().astimezone(tz) + if PY38: + return result + return self._new(result) + + def _new(self, result) -> "DateTime": raw = result.isoformat() return DateTime( @@ -819,6 +966,10 @@ def __init__( self._raw = raw + def unwrap(self) -> date: + (year, month, day, _, _) = self._getstate() + return date(year, month, day) + @property def discriminant(self) -> int: return 6 @@ -849,6 +1000,9 @@ def __sub__(self, other): return result + def replace(self, *args: Any, **kwargs: Any) -> date: + return self._new(super().replace(*args, **kwargs)) + def _new(self, result): raw = result.isoformat() @@ -888,6 +1042,10 @@ def __init__( self._raw = raw + def unwrap(self) -> time: + (hour, minute, second, microsecond, tzinfo, _, _) = self._getstate() + return time(hour, minute, second, microsecond, tzinfo) + @property def discriminant(self) -> int: return 7 @@ -899,6 +1057,22 @@ def value(self) -> time: def as_string(self) -> str: return self._raw + def replace(self, *args: Any, **kwargs: Any) -> time: + return self._new(super().replace(*args, **kwargs)) + + def _new(self, result): + raw = result.isoformat() + + return Time( + result.hour, + result.minute, + result.second, + result.microsecond, + result.tzinfo, + self._trivia, + raw, + ) + def _getstate(self, protocol: int = 3) -> tuple: return ( self.hour, @@ -911,22 +1085,93 @@ def _getstate(self, protocol: int = 3) -> tuple: ) +class _ArrayItemGroup: + __slots__ = ("value", "indent", "comma", "comment") + + def __init__( + self, + value: Optional[Item] = None, + indent: Optional[Whitespace] = None, + comma: Optional[Whitespace] = None, + comment: Optional[Comment] = None, + ) -> None: + self.value = value + self.indent = indent + self.comma = comma + self.comment = comment + + def __iter__(self) -> Iterator[Item]: + return filter( + lambda x: x is not None, (self.indent, self.value, self.comma, self.comment) + ) + + def __repr__(self) -> str: + return repr(tuple(self)) + + def is_whitespace(self) -> bool: + return self.value is None and self.comment is None + + def __bool__(self) -> bool: + try: + next(iter(self)) + except StopIteration: + return False + return True + + class Array(Item, _CustomList): """ An array literal """ - def __init__(self, value: list, trivia: Trivia, multiline: bool = False) -> None: + def __init__( + self, value: List[Item], trivia: Trivia, multiline: bool = False + ) -> None: super().__init__(trivia) - self._index_map: Dict[int, int] = {} list.__init__( - self, [v.value for v in value if not isinstance(v, (Whitespace, Comment))] + self, + [v for v in value if not isinstance(v, (Whitespace, Comment, Null))], ) - - self._value = value + self._index_map: Dict[int, int] = {} + self._value = self._group_values(value) self._multiline = multiline self._reindex() + def _group_values(self, value: List[Item]) -> List[_ArrayItemGroup]: + """Group the values into (indent, value, comma, comment) tuples""" + groups = [] + this_group = _ArrayItemGroup() + for item in value: + if isinstance(item, Whitespace): + if "," not in item.s: + groups.append(this_group) + this_group = _ArrayItemGroup(indent=item) + else: + if this_group.value is None: + # when comma is met and no value is provided, add a dummy Null + this_group.value = Null() + this_group.comma = item + elif isinstance(item, Comment): + if this_group.value is None: + this_group.value = Null() + this_group.comment = item + elif this_group.value is None: + this_group.value = item + else: + groups.append(this_group) + this_group = _ArrayItemGroup(value=item) + groups.append(this_group) + return [group for group in groups if group] + + def unwrap(self) -> List[Any]: + unwrapped = [] + for v in self: + if hasattr(v, "unwrap"): + unwrapped.append(v.unwrap()) + else: + unwrapped.append(v) + return unwrapped + @property def discriminant(self) -> int: return 8 @@ -935,6 +1180,10 @@ def discriminant(self) -> int: def value(self) -> list: return self + def _iter_items(self) -> Iterator[Item]: + for v in self._value: + yield from v + def multiline(self, multiline: bool) -> "Array": """Change the array to display in multiline or not. @@ -956,14 +1205,20 @@ def multiline(self, multiline: bool) -> "Array": def as_string(self) -> str: if not self._multiline or not self._value: - return "[{}]".format("".join(v.as_string() for v in self._value)) - - s = "[\n" + self.trivia.indent + " " * 4 - s += (",\n" + self.trivia.indent + " " * 4).join( - v.as_string() for v in self._value if not isinstance(v, Whitespace) + return f'[{"".join(v.as_string() for v in self._iter_items())}]' + + s = "[\n" + s += "".join( + self.trivia.indent + + " " * 4 + + v.value.as_string() + + ("," if not isinstance(v.value, Null) else "") + + (v.comment.as_string() if v.comment is not None else "") + + "\n" + for v in self._value + if v.value is not None ) - s += ",\n" - s += "]" + s += self.trivia.indent + "]" return s @@ -971,7 +1226,7 @@ def _reindex(self) -> None: self._index_map.clear() index = 0 for i, v in enumerate(self._value): - if isinstance(v, (Whitespace, Comment)): + if v.value is None or isinstance(v.value, Null): continue self._index_map[index] = i index += 1 @@ -1000,156 +1255,167 @@ def add_line( 4, 5, 6, ] """ - values = self._value[:] - new_values = [] - - def append_item(el: Item) -> None: - if not values: - return values.append(el) - last_el = values[-1] - if ( - isinstance(el, Whitespace) - and "," not in el.s - and isinstance(last_el, Whitespace) - and "," not in last_el.s - ): - values[-1] = Whitespace(last_el.s + el.s) - else: - values.append(el) - - if newline: - append_item(Whitespace("\n")) - if indent: - append_item(Whitespace(indent)) + new_values: List[Item] = [] + first_indent = f"\n{indent}" if newline else indent + if first_indent: + new_values.append(Whitespace(first_indent)) + whitespace = "" + data_values = [] for i, el in enumerate(items): - el = item(el, _parent=self) - if isinstance(el, Comment) or add_comma and isinstance(el, Whitespace): - raise ValueError(f"item type {type(el)} is not allowed") - if not isinstance(el, Whitespace): - new_values.append(el.value) - append_item(el) - if add_comma: - append_item(Whitespace(",")) - if i != len(items) - 1: - append_item(Whitespace(" ")) + it = item(el, _parent=self) + if isinstance(it, Comment) or add_comma and isinstance(el, Whitespace): + raise ValueError(f"item type {type(it)} is not allowed in add_line") + if not isinstance(it, Whitespace): + if whitespace: + new_values.append(Whitespace(whitespace)) + whitespace = "" + new_values.append(it) + data_values.append(it.value) + if add_comma: + new_values.append(Whitespace(",")) + if i != len(items) - 1: + new_values.append(Whitespace(" ")) + elif "," not in it.s: + whitespace += it.s + else: + new_values.append(it) + if whitespace: + new_values.append(Whitespace(whitespace)) if comment: indent = " " if items else "" - append_item( + new_values.append( Comment(Trivia(indent=indent, comment=f"# {comment}", trail="")) ) - # Atomic manipulation - self._value[:] = values - list.extend(self, new_values) + list.extend(self, data_values) + if len(self._value) > 0: + last_item = self._value[-1] + last_value_item = next( + ( + v + for v in self._value[::-1] + if v.value is not None and not isinstance(v.value, Null) + ), + None, + ) + if last_value_item is not None: + last_value_item.comma = Whitespace(",") + if last_item.is_whitespace(): + self._value[-1:-1] = self._group_values(new_values) + else: + self._value.extend(self._group_values(new_values)) + else: + self._value.extend(self._group_values(new_values)) self._reindex() def clear(self) -> None: """Clear the array.""" list.clear(self) - - self._value.clear() self._index_map.clear() + self._value.clear() def __len__(self) -> int: return list.__len__(self) def __getitem__(self, key: Union[int, slice]) -> Any: - return list.__getitem__(self, key) + rv = cast(Item, list.__getitem__(self, key)) + if rv.is_boolean(): + return bool(rv) + return rv def __setitem__(self, key: Union[int, slice], value: Any) -> Any: it = item(value, _parent=self) - list.__setitem__(self, key, it.value) + list.__setitem__(self, key, it) if isinstance(key, slice): raise ValueError("slice assignment is not supported") if key < 0: key += len(self) - self._value[self._index_map[key]] = it + self._value[self._index_map[key]].value = it def insert(self, pos: int, value: Any) -> None: it = item(value, _parent=self) length = len(self) if not isinstance(it, (Comment, Whitespace)): - list.insert(self, pos, it.value) + list.insert(self, pos, it) if pos < 0: pos += length if pos < 0: pos = 0 - items = [it] - idx = 0 + idx = 0 # insert position of the self._value list + default_indent = " " if pos < length: try: idx = self._index_map[pos] - except KeyError: - raise IndexError("list index out of range") - if not isinstance(it, (Whitespace, Comment)): - items.append(Whitespace(",")) + except KeyError as e: + raise IndexError("list index out of range") from e else: idx = len(self._value) + if idx >= 1 and self._value[idx - 1].is_whitespace(): + # The last item is a pure whitespace(\n ), insert before it + idx -= 1 + if ( + self._value[idx].indent is not None + and "\n" in self._value[idx].indent.s + ): + default_indent = "\n " + indent: Optional[Item] = None + comma: Optional[Item] = Whitespace(",") if pos < length else None + if idx < len(self._value) and not self._value[idx].is_whitespace(): + # Prefer to copy the indentation from the item after + indent = self._value[idx].indent if idx > 0: last_item = self._value[idx - 1] - if isinstance(last_item, Whitespace) and "," not in last_item.s: - # the item has an indent, copy that - idx -= 1 - ws = last_item.s - if isinstance(it, Whitespace) and "," not in it.s: - # merge the whitespace - self._value[idx] = Whitespace(ws + it.s) - return - else: - ws = "" - has_newline = bool(set(ws) & set(TOMLChar.NL)) - has_space = ws and ws[-1] in TOMLChar.SPACES - if not has_space: - # four spaces for multiline array and single space otherwise - ws += " " if has_newline else " " - items.insert(0, Whitespace(ws)) - self._value[idx:idx] = items - i = idx - 1 - if pos > 0: # Check if the last item ends with a comma - while i >= 0 and isinstance(self._value[i], (Whitespace, Comment)): - if isinstance(self._value[i], Whitespace) and "," in self._value[i].s: - break - i -= 1 - else: - self._value.insert(i + 1, Whitespace(",")) - + if indent is None: + indent = last_item.indent + if not isinstance(last_item.value, Null) and "\n" in default_indent: + # Copy the comma from the last item if 1) it contains a value and + # 2) the array is multiline + comma = last_item.comma + if last_item.comma is None and not isinstance(last_item.value, Null): + # Add comma to the last item to separate it from the following items. + last_item.comma = Whitespace(",") + if indent is None and (idx > 0 or "\n" in default_indent): + # apply default indent if it isn't the first item or the array is multiline. + indent = Whitespace(default_indent) + new_item = _ArrayItemGroup(value=it, indent=indent, comma=comma) + self._value.insert(idx, new_item) self._reindex() def __delitem__(self, key: Union[int, slice]): length = len(self) list.__delitem__(self, key) - def get_indice_to_remove(idx: int) -> Iterable[int]: - try: - real_idx = self._index_map[idx] - except KeyError: - raise IndexError("list index out of range") - yield real_idx - for i in range(real_idx + 1, len(self._value)): - if isinstance(self._value[i], Whitespace): - yield i - else: - break - - indexes = set() if isinstance(key, slice): - for idx in range(key.start or 0, key.stop or length, key.step or 1): - indexes.update(get_indice_to_remove(idx)) + indices_to_remove = list( + range(key.start or 0, key.stop or length, key.step or 1) + ) else: - indexes.update(get_indice_to_remove(length + key if key < 0 else key)) - for i in sorted(indexes, reverse=True): - del self._value[i] - while self._value and isinstance(self._value[-1], Whitespace): - self._value.pop() - self._reindex() + indices_to_remove = [length + key if key < 0 else key] + for i in sorted(indices_to_remove, reverse=True): + try: + idx = self._index_map[i] + except KeyError as e: + if not isinstance(key, slice): + raise IndexError("list index out of range") from e + else: + del self._value[idx] + if ( + idx == 0 + and len(self._value) > 0 + and "\n" not in self._value[idx].indent.s + ): + # Remove the indentation of the first item if not newline + self._value[idx].indent = None + if len(self._value) > 0: + v = self._value[-1] + if not v.is_whitespace(): + # remove the comma of the last item + v.comma = None - def __str__(self): - return str( - [v.value for v in self._value if not isinstance(v, (Whitespace, Comment))] - ) + self._reindex() def _getstate(self, protocol=3): - return self._value, self._trivia + return list(self._iter_items()), self._trivia, self._multiline AT = TypeVar("AT", bound="AbstractTable") @@ -1167,6 +1433,17 @@ def __init__(self, value: "container.Container", trivia: Trivia): if k is not None: dict.__setitem__(self, k.key, v) + def unwrap(self) -> Dict[str, Any]: + unwrapped = {} + for k, v in self.items(): + if isinstance(k, Key): + k = k.key + if hasattr(v, "unwrap"): + v = v.unwrap() + unwrapped[k] = v + + return unwrapped + @property def value(self) -> "container.Container": return self._value @@ -1238,7 +1515,7 @@ def __getitem__(self, key: Union[Key, str]) -> Item: def __setitem__(self, key: Union[Key, str], value: Any) -> None: if not isinstance(value, Item): - value = item(value) + value = item(value, _parent=self) is_replace = key in self self._value[key] = value @@ -1272,7 +1549,7 @@ def __init__( value: "container.Container", trivia: Trivia, is_aot_element: bool, - is_super_table: bool = False, + is_super_table: Optional[bool] = None, name: Optional[str] = None, display_name: Optional[str] = None, ) -> None: @@ -1302,7 +1579,7 @@ def append(self, key, _item): Appends a (key, item) to the table. """ if not isinstance(_item, Item): - _item = item(_item) + _item = item(_item, _parent=self) self._value.append(key, _item) @@ -1351,7 +1628,13 @@ def is_aot_element(self) -> bool: def is_super_table(self) -> bool: """A super table is the intermediate parent of a nested table as in [a.b.c]. If true, it won't appear in the TOML representation.""" - return self._is_super_table + if self._is_super_table is not None: + return self._is_super_table + # If the table has only one child and that child is a table, then it is a super table. + if len(self) != 1: + return False + only_child = next(iter(self.values())) + return isinstance(only_child, (Table, AoT)) def as_string(self) -> str: return self._value.as_string() @@ -1413,7 +1696,7 @@ def append(self, key, _item): Appends a (key, item) to the table. """ if not isinstance(_item, Item): - _item = item(_item) + _item = item(_item, _parent=self) if not isinstance(_item, (Whitespace, Comment)): if not _item.trivia.indent and len(self._value) > 0 and not self._new: @@ -1433,6 +1716,14 @@ def append(self, key, _item): def as_string(self) -> str: buf = "{" + last_item_idx = next( + ( + i + for i in range(len(self._value.body) - 1, -1, -1) + if self._value.body[i][0] is not None + ), + None, + ) for i, (k, v) in enumerate(self._value.body): if k is None: if i == len(self._value.body) - 1: @@ -1445,16 +1736,17 @@ def as_string(self) -> str: continue - buf += "{}{}{}{}{}{}".format( - v.trivia.indent, - k.as_string() + ("." if k.is_dotted() else ""), - k.sep, - v.as_string(), - v.trivia.comment, - v.trivia.trail.replace("\n", ""), + v_trivia_trail = v.trivia.trail.replace("\n", "") + buf += ( + f"{v.trivia.indent}" + f'{k.as_string() + ("." if k.is_dotted() else "")}' + f"{k.sep}" + f"{v.as_string()}" + f"{v.trivia.comment}" + f"{v_trivia_trail}" ) - if i != len(self._value.body) - 1: + if last_item_idx is not None and i < last_item_idx: buf += "," if self._new: buf += " " @@ -1489,6 +1781,9 @@ def __init__(self, t: StringType, _: str, original: str, trivia: Trivia) -> None self._t = t self._original = original + def unwrap(self) -> str: + return str(self) + @property def discriminant(self) -> int: return 11 @@ -1500,22 +1795,33 @@ def value(self) -> str: def as_string(self) -> str: return f"{self._t.value}{decode(self._original)}{self._t.value}" - def __add__(self, other): + def __add__(self: ItemT, other: str) -> ItemT: + if not isinstance(other, str): + return NotImplemented result = super().__add__(other) + original = self._original + getattr(other, "_original", other) - return self._new(result) - - def __sub__(self, other): - result = super().__sub__(other) + return self._new(result, original) - return self._new(result) - - def _new(self, result): - return String(self._t, result, result, self._trivia) + def _new(self, result: str, original: str) -> "String": + return String(self._t, result, original, self._trivia) def _getstate(self, protocol=3): return self._t, str(self), self._original, self._trivia + @classmethod + def from_raw(cls, value: str, type_=StringType.SLB, escape=True) -> "String": + value = decode(value) + + invalid = type_.invalid_sequences + if any(c in value for c in invalid): + raise InvalidStringError(value, invalid, type_.value) + + escaped = type_.escaped_sequences + string_value = escape_string(value, escaped) if escape and escaped else value + + return cls(type_, decode(value), string_value, Trivia()) + class AoT(Item, _CustomList): """ @@ -1534,6 +1840,15 @@ def __init__( for table in body: self.append(table) + def unwrap(self) -> List[Dict[str, Any]]: + unwrapped = [] + for t in self._body: + if hasattr(t, "unwrap"): + unwrapped.append(t.unwrap()) + else: + unwrapped.append(t) + return unwrapped + @property def body(self) -> List[Table]: return self._body @@ -1625,6 +1940,9 @@ class Null(Item): def __init__(self) -> None: pass + def unwrap(self) -> None: + return None + @property def discriminant(self) -> int: return -1 @@ -1636,5 +1954,5 @@ def value(self) -> None: def as_string(self) -> str: return "" - def _getstate(self, protocol=3): - return tuple() + def _getstate(self, protocol=3) -> tuple: + return () diff --git a/pipenv/vendor/tomlkit/parser.py b/pipenv/vendor/tomlkit/parser.py index e1f64dff64..5e30de5757 100644 --- a/pipenv/vendor/tomlkit/parser.py +++ b/pipenv/vendor/tomlkit/parser.py @@ -1,3 +1,4 @@ +import datetime import re import string @@ -7,48 +8,48 @@ from typing import Type from typing import Union -from ._compat import decode -from ._utils import RFC_3339_LOOSE -from ._utils import _escaped -from ._utils import parse_rfc3339 -from .container import Container -from .exceptions import EmptyKeyError -from .exceptions import EmptyTableNameError -from .exceptions import InternalParserError -from .exceptions import InvalidCharInStringError -from .exceptions import InvalidControlChar -from .exceptions import InvalidDateError -from .exceptions import InvalidDateTimeError -from .exceptions import InvalidNumberError -from .exceptions import InvalidTimeError -from .exceptions import InvalidUnicodeValueError -from .exceptions import ParseError -from .exceptions import UnexpectedCharError -from .exceptions import UnexpectedEofError -from .items import AoT -from .items import Array -from .items import Bool -from .items import BoolType -from .items import Comment -from .items import Date -from .items import DateTime -from .items import Float -from .items import InlineTable -from .items import Integer -from .items import Item -from .items import Key -from .items import KeyType -from .items import Null -from .items import SingleKey -from .items import String -from .items import StringType -from .items import Table -from .items import Time -from .items import Trivia -from .items import Whitespace -from .source import Source -from .toml_char import TOMLChar -from .toml_document import TOMLDocument +from pipenv.vendor.tomlkit._compat import decode +from pipenv.vendor.tomlkit._utils import RFC_3339_LOOSE +from pipenv.vendor.tomlkit._utils import _escaped +from pipenv.vendor.tomlkit._utils import parse_rfc3339 +from pipenv.vendor.tomlkit.container import Container +from pipenv.vendor.tomlkit.exceptions import EmptyKeyError +from pipenv.vendor.tomlkit.exceptions import EmptyTableNameError +from pipenv.vendor.tomlkit.exceptions import InternalParserError +from pipenv.vendor.tomlkit.exceptions import InvalidCharInStringError +from pipenv.vendor.tomlkit.exceptions import InvalidControlChar +from pipenv.vendor.tomlkit.exceptions import InvalidDateError +from pipenv.vendor.tomlkit.exceptions import InvalidDateTimeError +from pipenv.vendor.tomlkit.exceptions import InvalidNumberError +from pipenv.vendor.tomlkit.exceptions import InvalidTimeError +from pipenv.vendor.tomlkit.exceptions import InvalidUnicodeValueError +from pipenv.vendor.tomlkit.exceptions import ParseError +from pipenv.vendor.tomlkit.exceptions import UnexpectedCharError +from pipenv.vendor.tomlkit.exceptions import UnexpectedEofError +from pipenv.vendor.tomlkit.items import AoT +from pipenv.vendor.tomlkit.items import Array +from pipenv.vendor.tomlkit.items import Bool +from pipenv.vendor.tomlkit.items import BoolType +from pipenv.vendor.tomlkit.items import Comment +from pipenv.vendor.tomlkit.items import Date +from pipenv.vendor.tomlkit.items import DateTime +from pipenv.vendor.tomlkit.items import Float +from pipenv.vendor.tomlkit.items import InlineTable +from pipenv.vendor.tomlkit.items import Integer +from pipenv.vendor.tomlkit.items import Item +from pipenv.vendor.tomlkit.items import Key +from pipenv.vendor.tomlkit.items import KeyType +from pipenv.vendor.tomlkit.items import Null +from pipenv.vendor.tomlkit.items import SingleKey +from pipenv.vendor.tomlkit.items import String +from pipenv.vendor.tomlkit.items import StringType +from pipenv.vendor.tomlkit.items import Table +from pipenv.vendor.tomlkit.items import Time +from pipenv.vendor.tomlkit.items import Trivia +from pipenv.vendor.tomlkit.items import Whitespace +from pipenv.vendor.tomlkit.source import Source +from pipenv.vendor.tomlkit.toml_char import TOMLChar +from pipenv.vendor.tomlkit.toml_document import TOMLDocument CTRL_I = 0x09 # Tab @@ -146,7 +147,10 @@ def parse(self) -> TOMLDocument: key, value = item if (key is not None and key.is_multi()) or not self._merge_ws(value, body): # We actually have a table - body.append(key, value) + try: + body.append(key, value) + except Exception as e: + raise self.parse_error(ParseError, str(e)) from e self.mark() @@ -157,7 +161,10 @@ def parse(self) -> TOMLDocument: # along with it. value = self._parse_aot(value, key) - body.append(key, value) + try: + body.append(key, value) + except Exception as e: + raise self.parse_error(ParseError, str(e)) from e body.parsing(False) @@ -226,7 +233,7 @@ def _parse_item(self) -> Optional[Tuple[Optional[Key], Item]]: # Found a table, delegate to the calling function. return else: - # Begining of a KV pair. + # Beginning of a KV pair. # Return to beginning of whitespace so it gets included # as indentation for the KV about to be parsed. state.restore = True @@ -464,6 +471,7 @@ def _parse_value(self) -> Item: # datetime try: dt = parse_rfc3339(raw) + assert isinstance(dt, datetime.datetime) return DateTime( dt.year, dt.month, @@ -482,17 +490,20 @@ def _parse_value(self) -> Item: if m.group(1): try: dt = parse_rfc3339(raw) + assert isinstance(dt, datetime.date) date = Date(dt.year, dt.month, dt.day, trivia, raw) self.mark() while self._current not in "\t\n\r#,]}" and self.inc(): pass time_raw = self.extract() - if not time_raw.strip(): - trivia.comment_ws = time_raw + time_part = time_raw.rstrip() + trivia.comment_ws = time_raw[len(time_part) :] + if not time_part: return date - dt = parse_rfc3339(raw + time_raw) + dt = parse_rfc3339(raw + time_part) + assert isinstance(dt, datetime.datetime) return DateTime( dt.year, dt.month, @@ -503,7 +514,7 @@ def _parse_value(self) -> Item: dt.microsecond, dt.tzinfo, trivia, - raw + time_raw, + raw + time_part, ) except ValueError: raise self.parse_error(InvalidDateError) @@ -511,6 +522,7 @@ def _parse_value(self) -> Item: if m.group(5): try: t = parse_rfc3339(raw) + assert isinstance(t, datetime.time) return Time( t.hour, t.minute, @@ -672,10 +684,10 @@ def _parse_number(self, raw: str, trivia: Trivia) -> Optional[Item]: or sign and raw.startswith(".") ): - return + return None if raw.startswith(("0o", "0x", "0b")) and sign: - return + return None digits = "[0-9]" base = 10 @@ -693,14 +705,14 @@ def _parse_number(self, raw: str, trivia: Trivia) -> Optional[Item]: clean = re.sub(f"(?i)(?<={digits})_(?={digits})", "", raw).lower() if "_" in clean: - return + return None if ( clean.endswith(".") or not clean.startswith("0x") and clean.split("e", 1)[0].endswith(".") ): - return + return None try: return Integer(int(sign + clean, base), trivia, sign + raw) @@ -708,7 +720,7 @@ def _parse_number(self, raw: str, trivia: Trivia) -> Optional[Item]: try: return Float(float(sign + clean), trivia, sign + raw) except ValueError: - return + return None def _parse_literal_string(self) -> String: with self._state: @@ -802,9 +814,7 @@ def _parse_string(self, delim: StringType) -> String: delim.is_singleline() and not escaped and (code == CHR_DEL or code <= CTRL_CHAR_LIMIT and code != CTRL_I) - ): - raise self.parse_error(InvalidControlChar, code, "strings") - elif ( + ) or ( delim.is_multiline() and not escaped and ( @@ -901,8 +911,6 @@ def _parse_table( raise self.parse_error(UnexpectedEofError) elif self._current != "]": raise self.parse_error(UnexpectedCharError, self._current) - elif not key.key.strip(): - raise self.parse_error(EmptyTableNameError) key.sep = "" full_key = key @@ -916,7 +924,7 @@ def _parse_table( if parent_name: parent_name_parts = tuple(parent_name) else: - parent_name_parts = tuple() + parent_name_parts = () if len(name_parts) > len(parent_name_parts) + 1: missing_table = True @@ -939,6 +947,7 @@ def _parse_table( is_aot, name=name_parts[0].key if name_parts else key.key, display_name=full_key.as_string(), + is_super_table=False, ) if len(name_parts) > 1: @@ -960,10 +969,9 @@ def _parse_table( key = name_parts[0] for i, _name in enumerate(name_parts[1:]): - if _name in table: - child = table[_name] - else: - child = Table( + child = table.get( + _name, + Table( Container(True), Trivia(indent, cws, comment, trail), is_aot and i == len(name_parts) - 2, @@ -972,7 +980,8 @@ def _parse_table( display_name=full_key.as_string() if i == len(name_parts) - 2 else None, - ) + ), + ) if is_aot and i == len(name_parts) - 2: table.raw_append(_name, AoT([child], name=table.name, parsed=True)) diff --git a/pipenv/vendor/tomlkit/source.py b/pipenv/vendor/tomlkit/source.py index 6f82d946c0..93d8921f19 100644 --- a/pipenv/vendor/tomlkit/source.py +++ b/pipenv/vendor/tomlkit/source.py @@ -4,9 +4,9 @@ from typing import Tuple from typing import Type -from .exceptions import ParseError -from .exceptions import UnexpectedCharError -from .toml_char import TOMLChar +from pipenv.vendor.tomlkit.exceptions import ParseError +from pipenv.vendor.tomlkit.exceptions import UnexpectedCharError +from pipenv.vendor.tomlkit.toml_char import TOMLChar class _State: @@ -129,11 +129,7 @@ def inc_n(self, n: int, exception: Optional[Type[ParseError]] = None) -> bool: Increments the parser by n characters if the end of the input has not been reached. """ - for _ in range(n): - if not self.inc(exception=exception): - return False - - return True + return all(self.inc(exception=exception) for _ in range(n)) def consume(self, chars, min=0, max=-1): """ diff --git a/pipenv/vendor/tomlkit/toml_char.py b/pipenv/vendor/tomlkit/toml_char.py index 11e5385d1b..b4bb4110c5 100644 --- a/pipenv/vendor/tomlkit/toml_char.py +++ b/pipenv/vendor/tomlkit/toml_char.py @@ -1,7 +1,5 @@ import string -from functools import lru_cache - class TOMLChar(str): def __init__(self, c): @@ -17,42 +15,36 @@ def __init__(self, c): NL = "\n\r" WS = SPACES + NL - @lru_cache(maxsize=None) def is_bare_key_char(self) -> bool: """ Whether the character is a valid bare key name or not. """ return self in self.BARE - @lru_cache(maxsize=None) def is_kv_sep(self) -> bool: """ - Whether the character is a valid key/value separator ot not. + Whether the character is a valid key/value separator or not. """ return self in self.KV - @lru_cache(maxsize=None) def is_int_float_char(self) -> bool: """ Whether the character if a valid integer or float value character or not. """ return self in self.NUMBER - @lru_cache(maxsize=None) def is_ws(self) -> bool: """ Whether the character is a whitespace character or not. """ return self in self.WS - @lru_cache(maxsize=None) def is_nl(self) -> bool: """ Whether the character is a new line character or not. """ return self in self.NL - @lru_cache(maxsize=None) def is_spaces(self) -> bool: """ Whether the character is a space or not diff --git a/pipenv/vendor/tomlkit/toml_document.py b/pipenv/vendor/tomlkit/toml_document.py index b485e3029e..44d0b33eb0 100644 --- a/pipenv/vendor/tomlkit/toml_document.py +++ b/pipenv/vendor/tomlkit/toml_document.py @@ -1,4 +1,4 @@ -from .container import Container +from pipenv.vendor.tomlkit.container import Container class TOMLDocument(Container): diff --git a/pipenv/vendor/tomlkit/toml_file.py b/pipenv/vendor/tomlkit/toml_file.py index 47c9f2c238..3148124f69 100644 --- a/pipenv/vendor/tomlkit/toml_file.py +++ b/pipenv/vendor/tomlkit/toml_file.py @@ -1,5 +1,18 @@ -from .api import loads -from .toml_document import TOMLDocument +import os +import re + +from typing import TYPE_CHECKING + +from pipenv.vendor.tomlkit.api import loads +from pipenv.vendor.tomlkit.toml_document import TOMLDocument + + +if TYPE_CHECKING: + from _typeshed import StrPath as _StrPath +else: + from typing import Union + + _StrPath = Union[str, os.PathLike] class TOMLFile: @@ -9,15 +22,37 @@ class TOMLFile: :param path: path to the TOML file """ - def __init__(self, path: str) -> None: + def __init__(self, path: _StrPath) -> None: self._path = path + self._linesep = os.linesep def read(self) -> TOMLDocument: """Read the file content as a :class:`tomlkit.toml_document.TOMLDocument`.""" with open(self._path, encoding="utf-8", newline="") as f: - return loads(f.read()) + content = f.read() + + # check if consistent line endings + num_newline = content.count("\n") + if num_newline > 0: + num_win_eol = content.count("\r\n") + if num_win_eol == num_newline: + self._linesep = "\r\n" + elif num_win_eol == 0: + self._linesep = "\n" + else: + self._linesep = "mixed" + + return loads(content) def write(self, data: TOMLDocument) -> None: """Write the TOMLDocument to the file.""" + content = data.as_string() + + # apply linesep + if self._linesep == "\n": + content = content.replace("\r\n", "\n") + elif self._linesep == "\r\n": + content = re.sub(r"(?