diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6aa0dbc371..d48e409c45 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,7 +50,7 @@ jobs: GIT_ASK_YESNO: "false" steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - run: | @@ -82,7 +82,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -136,7 +136,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install -U build twine diff --git a/pipenv/vendor/plette/__init__.py b/pipenv/vendor/plette/__init__.py index 7734698d76..dbd68f55e2 100644 --- a/pipenv/vendor/plette/__init__.py +++ b/pipenv/vendor/plette/__init__.py @@ -3,7 +3,7 @@ "Lockfile", "Pipfile", ] -__version__ = '0.4.4' +__version__ = '2.0.2' from .lockfiles import Lockfile from .pipfiles import Pipfile diff --git a/pipenv/vendor/plette/lockfiles.py b/pipenv/vendor/plette/lockfiles.py index e8c3091e1b..fcb8779d03 100644 --- a/pipenv/vendor/plette/lockfiles.py +++ b/pipenv/vendor/plette/lockfiles.py @@ -4,7 +4,7 @@ import collections.abc as collections_abc -from .models import DataView, Meta, PackageCollection +from .models import DataModel, Meta, PackageCollection class _LockFileEncoder(json.JSONEncoder): @@ -52,7 +52,7 @@ def _copy_jsonsafe(value): return str(value) -class Lockfile(DataView): +class Lockfile(DataModel): """Representation of a Pipfile.lock. """ __SCHEMA__ = { @@ -63,7 +63,6 @@ class Lockfile(DataView): @classmethod def validate(cls, data): - super(Lockfile, cls).validate(data) for key, value in data.items(): if key == "_meta": Meta.validate(value) @@ -116,7 +115,7 @@ def __getitem__(self, key): return value def __setitem__(self, key, value): - if isinstance(value, DataView): + if isinstance(value, DataModel): self._data[key] = value._data else: self._data[key] = value diff --git a/pipenv/vendor/plette/models/__init__.py b/pipenv/vendor/plette/models/__init__.py index 20c2899330..9f9b1637da 100644 --- a/pipenv/vendor/plette/models/__init__.py +++ b/pipenv/vendor/plette/models/__init__.py @@ -1,13 +1,13 @@ __all__ = [ - "DataView", "DataViewCollection", "DataViewMapping", "DataViewSequence", - "validate", "ValidationError", + "DataModel", "DataModelCollection", "DataModelMapping", "DataModelSequence", + "DataValidationError", "Hash", "Package", "Requires", "Source", "Script", "Meta", "PackageCollection", "ScriptCollection", "SourceCollection", ] from .base import ( - DataView, DataViewCollection, DataViewMapping, DataViewSequence, - validate, ValidationError, + DataModel, DataModelCollection, DataModelMapping, DataModelSequence, + DataValidationError, ) from .hashes import Hash diff --git a/pipenv/vendor/plette/models/base.py b/pipenv/vendor/plette/models/base.py index 72cf372e72..c3d6937f09 100644 --- a/pipenv/vendor/plette/models/base.py +++ b/pipenv/vendor/plette/models/base.py @@ -1,49 +1,8 @@ -try: - import cerberus -except ImportError: - cerberus = None - - -class ValidationError(ValueError): - def __init__(self, value, validator): - super(ValidationError, self).__init__(value) - self.validator = validator - self.value = value - - def __str__(self): - return '{}\n{}'.format( - self.value, - '\n'.join( - '{}: {}'.format(k, e) - for k, errors in self.validator.errors.items() - for e in errors - ) - ) - - -VALIDATORS = {} - +class DataValidationError(ValueError): + pass -def validate(cls, data): - if not cerberus: # Skip validation if Cerberus is not available. - return - schema = cls.__SCHEMA__ - key = id(schema) - try: - v = VALIDATORS[key] - except KeyError: - v = VALIDATORS[key] = cerberus.Validator(schema, allow_unknown=True) - if v.validate(data, normalize=False): - return - raise ValidationError(data, v) - -class DataView(object): - """A "view" to a data. - - Validates the input mapping on creation. A subclass is expected to - provide a `__SCHEMA__` class attribute specifying a validator schema. - """ +class DataModel: def __init__(self, data): self.validate(data) @@ -78,15 +37,24 @@ def get(self, key, default=None): @classmethod def validate(cls, data): - return validate(cls, data) + for k, v in cls.__SCHEMA__.items(): + if k not in data: + raise DataValidationError(f"Missing required field: {k}") + if not isinstance(data[k], v): + raise DataValidationError(f"Invalid type for field {k}: {type(data[k])}") + if hasattr(cls, "__OPTIONAL__"): + for k, v in cls.__OPTIONAL__.items(): + if k in data and not isinstance(data[k], v): + raise DataValidationError(f"Invalid type for field {k}: {type(data[k])}") -class DataViewCollection(DataView): + +class DataModelCollection(DataModel): """A homogeneous collection of data views. Subclasses are expected to assign a class attribute `item_class` to specify the type of items it contains. This class will be used to coerce return - values when accessed. The item class should conform to the `DataView` + values when accessed. The item class should conform to the `DataModel` protocol. You should not instantiate an instance from this class, but from one of its @@ -105,7 +73,7 @@ def __getitem__(self, key): return self.item_class(self._data[key]) def __setitem__(self, key, value): - if isinstance(value, DataView): + if isinstance(value, DataModel): value = value._data self._data[key] = value @@ -113,50 +81,50 @@ def __delitem__(self, key): del self._data[key] -class DataViewMapping(DataViewCollection): - """A mapping of data views. +class DataModelSequence(DataModelCollection): + """A sequence of data views. - The keys are primitive values, while values are instances of `item_class`. + Each entry is an instance of `item_class`. """ @classmethod def validate(cls, data): - for d in data.values(): + for d in data: cls.item_class.validate(d) def __iter__(self): - return iter(self._data) - - def keys(self): - return self._data.keys() + return (self.item_class(d) for d in self._data) - def values(self): - return [self[k] for k in self._data] + def __getitem__(self, key): + if isinstance(key, slice): + return type(self)(self._data[key]) + return super().__getitem__(key) - def items(self): - return [(k, self[k]) for k in self._data] + def append(self, value): + if isinstance(value, DataModel): + value = value._data + self._data.append(value) -class DataViewSequence(DataViewCollection): - """A sequence of data views. +class DataModelMapping(DataModelCollection): + """A mapping of data views. - Each entry is an instance of `item_class`. + The keys are primitive values, while values are instances of `item_class`. """ @classmethod def validate(cls, data): - for d in data: + for d in data.values(): cls.item_class.validate(d) def __iter__(self): - return (self.item_class(d) for d in self._data) + return iter(self._data) - def __getitem__(self, key): - if isinstance(key, slice): - return type(self)(self._data[key]) - return super(DataViewSequence, self).__getitem__(key) + def keys(self): + return self._data.keys() - def append(self, value): - if isinstance(value, DataView): - value = value._data - self._data.append(value) + def values(self): + return [self[k] for k in self._data] + + def items(self): + return [(k, self[k]) for k in self._data] diff --git a/pipenv/vendor/plette/models/hashes.py b/pipenv/vendor/plette/models/hashes.py index d35d312e33..75c4c2cb3d 100644 --- a/pipenv/vendor/plette/models/hashes.py +++ b/pipenv/vendor/plette/models/hashes.py @@ -1,28 +1,50 @@ -from .base import DataView +from .base import DataModel, DataValidationError -class Hash(DataView): +class Hash(DataModel): """A hash. """ + item_class = "Hash" + __SCHEMA__ = { - "__hash__": { - "type": "list", "minlength": 1, "maxlength": 1, - "schema": { - "type": "list", "minlength": 2, "maxlength": 2, - "schema": {"type": "string"}, - }, - }, } + __OPTIONAL__ = { + "name": str, + "md5": str, + "sha256": str, + "digest": str, + } + + def __init__(self, data): + self.validate(data) + self._data = data + if "name" in data: + self.name = data["name"] + try: + self.digest = data["digest"] + except KeyError: + self.digest = data["value"] + elif "md5" in data: + self.name = "md5" + self.digest = data["md5"] + elif "sha256" in data: + self.name = "sha256" + self.digest = data["sha256"] + @classmethod def validate(cls, data): - super(Hash, cls).validate({"__hash__": list(data.items())}) + for k, v in cls.__SCHEMA__.items(): + if k not in data: + raise DataValidationError(f"Missing required field: {k}") + if not isinstance(data[k], v): + raise DataValidationError(f"Invalid type for field {k}: {type(data[k])}") @classmethod def from_hash(cls, ins): """Interpolation to the hash result of `hashlib`. """ - return cls({ins.name: ins.hexdigest()}) + return cls(data={ins.name: ins.hexdigest()}) @classmethod def from_line(cls, value): @@ -30,7 +52,7 @@ def from_line(cls, value): name, value = value.split(":", 1) except ValueError: name = "sha256" - return cls({name: value}) + return cls(data={"name":name, "value": value}) def __eq__(self, other): if not isinstance(other, Hash): @@ -39,13 +61,9 @@ def __eq__(self, other): )) return self._data == other._data - @property - def name(self): - return next(iter(self._data.keys())) - @property def value(self): - return next(iter(self._data.values())) + return self.digest def as_line(self): return "{0[0]}:{0[1]}".format(next(iter(self._data.items()))) diff --git a/pipenv/vendor/plette/models/packages.py b/pipenv/vendor/plette/models/packages.py index 8b0ebe4f61..fd67c90df3 100644 --- a/pipenv/vendor/plette/models/packages.py +++ b/pipenv/vendor/plette/models/packages.py @@ -1,7 +1,19 @@ -from .base import DataView +import pipenv.vendor.tomlkit as tomlkit +from .base import DataModel, DataValidationError -class Package(DataView): +class PackageSpecfiers(DataModel): + # TODO: one could add here more validation for path editable + # and more stuff which is currently allowed and undocumented + __SCHEMA__ = {} + __OPTIONAL__ = { + "editable": bool, + "version": str, + "extras": list + } + + +class Package(DataModel): """A package requirement specified in a Pipfile. This is the base class of variants appearing in either `[packages]` or @@ -11,22 +23,21 @@ class Package(DataView): # to have oneof_schema (at least I can't do it), so we wrap this in a # top-level key. The Requirement model class implements extra hacks to # make this work. - __SCHEMA__ = { - "__package__": { - "oneof_type": ["string", "dict"], - }, + __OPTIONAL__ = { + "PackageSpecfiers": (str, dict) } @classmethod def validate(cls, data): - # HACK: Make this validatable for Cerberus. See comments in validation - # side for more information. - super(Package, cls).validate({"__package__": data}) + if isinstance(data, (str, tomlkit.items.Float, tomlkit.items.Integer)): + return if isinstance(data, dict): - PackageSpecfiers.validate({"__specifiers__": data}) + PackageSpecfiers.validate(data) + else: + raise DataValidationError(f"invalid type for package data: {type(data)}") def __getattr__(self, key): - if isinstance(self._data, str): + if isinstance(self._data, (str, tomlkit.items.Float, tomlkit.items.Integer)): if key == "version": return self._data raise AttributeError(key) @@ -38,21 +49,8 @@ def __getattr__(self, key): def __setattr__(self, key, value): if key == "_data": - super(Package, self).__setattr__(key, value) + super().__setattr__(key, value) elif key == "version" and isinstance(self._data, str): self._data = value else: self._data[key] = value - -class PackageSpecfiers(DataView): - # TODO: one could add here more validation for path editable - # and more stuff which is currently allowed and undocumented - __SCHEMA__ = { - "__specifiers__": { - "type": "dict", - "schema":{ - "version": {"type": "string"}, - "extras": {"type": "list"}, - } - } - } diff --git a/pipenv/vendor/plette/models/scripts.py b/pipenv/vendor/plette/models/scripts.py index 69f1c9963c..9a77b442f0 100644 --- a/pipenv/vendor/plette/models/scripts.py +++ b/pipenv/vendor/plette/models/scripts.py @@ -1,37 +1,31 @@ import re import shlex -from .base import DataView +from .base import DataModel, DataValidationError -class Script(DataView): +class Script(DataModel): """Parse a script line (in Pipfile's [scripts] section). This always works in POSIX mode, even on Windows. """ - # This extra layer is intentional. Cerberus does not allow validation of - # non-mapping inputs, so we wrap this in a top-level key. The Script model - # class implements extra hacks to make this work. - __SCHEMA__ = { - "__script__": { - "oneof_type": ["string", "list"], "required": True, "empty": False, - "schema": {"type": "string"}, - }, + __OPTIONAL__ = { + "script": (str,list) } def __init__(self, data): - super(Script, self).__init__(data) + self.validate(data) if isinstance(data, str): data = shlex.split(data) - self._parts = [data[0]] - self._parts.extend(data[1:]) + self._parts = data[::] @classmethod def validate(cls, data): - # HACK: Make this validatable for Cerberus. See comments in validation - # side for more information. - return super(Script, cls).validate({"__script__": data}) - + if not data: + raise DataValidationError("Script cannot be empty") + for k, types in cls.__OPTIONAL__.items(): + if not isinstance(data, types): + raise DataValidationError(f"Invalid type for field {t}: {type(data[t])}") def __repr__(self): return "Script({0!r})".format(self._parts) diff --git a/pipenv/vendor/plette/models/sections.py b/pipenv/vendor/plette/models/sections.py index acaaa4dd86..82d893b05e 100644 --- a/pipenv/vendor/plette/models/sections.py +++ b/pipenv/vendor/plette/models/sections.py @@ -1,32 +1,30 @@ -from .base import DataView, DataViewMapping, DataViewSequence +from .base import DataModel, DataModelSequence, DataModelMapping from .hashes import Hash from .packages import Package from .scripts import Script from .sources import Source -class PackageCollection(DataViewMapping): +class PackageCollection(DataModelMapping): item_class = Package -class ScriptCollection(DataViewMapping): +class ScriptCollection(DataModelMapping): item_class = Script -class SourceCollection(DataViewSequence): +class SourceCollection(DataModelSequence): item_class = Source -class Requires(DataView): +class Requires(DataModel): """Representation of the `[requires]` section in a Pipfile.""" - __SCHEMA__ = { - "python_version": { - "type": "string", - }, - "python_full_version": { - "type": "string", - }, + __SCHEMA__ = {} + + __OPTIONAL__ = { + "python_version": str, + "python_full_version": str, } @property @@ -50,7 +48,8 @@ def python_full_version(self): "sources": SourceCollection, } -class PipfileSection(DataView): + +class PipfileSection(DataModel): """ Dummy pipfile validator that needs to be completed in a future PR @@ -61,31 +60,31 @@ class PipfileSection(DataView): def validate(cls, data): pass -class Meta(DataView): + +class Meta(DataModel): """Representation of the `_meta` section in a Pipfile.lock.""" __SCHEMA__ = { - "hash": {"type": "dict", "required": True}, - "pipfile-spec": {"type": "integer", "required": True, "min": 0}, - "requires": {"type": "dict", "required": True}, - "sources": {"type": "list", "required": True}, + "hash": "dict", + "pipfile-spec": "integer", + "requires": "dict", + "sources": "list" } @classmethod def validate(cls, data): - super(Meta, cls).validate(data) for key, klass in META_SECTIONS.items(): klass.validate(data[key]) def __getitem__(self, key): - value = super(Meta, self).__getitem__(key) + value = super().__getitem__(key) try: return META_SECTIONS[key](value) except KeyError: return value def __setitem__(self, key, value): - if isinstance(value, DataView): + if isinstance(value, DataModel): self._data[key] = value._data else: self._data[key] = value @@ -131,9 +130,9 @@ def sources(self, value): self["sources"] = value -class Pipenv(DataView): +class Pipenv(DataModel): """Represent the [pipenv] section in Pipfile""" - - __SCHEMA__ = { - "allow_prereleases": {"type": "boolean", "required": False}, + __SCHEMA__ = {} + __OPTIONAL__ = { + "allow_prereleases": bool, } diff --git a/pipenv/vendor/plette/models/sources.py b/pipenv/vendor/plette/models/sources.py index dc2529a064..95fc56ae76 100644 --- a/pipenv/vendor/plette/models/sources.py +++ b/pipenv/vendor/plette/models/sources.py @@ -1,9 +1,9 @@ import os -from .base import DataView +from .base import DataModel -class Source(DataView): +class Source(DataModel): """Information on a "simple" Python package index. This could be PyPI, or a self-hosted index server, etc. The server @@ -11,9 +11,9 @@ class Source(DataView): package API. """ __SCHEMA__ = { - "name": {"type": "string", "required": True}, - "url": {"type": "string", "required": True}, - "verify_ssl": {"type": "boolean", "required": True}, + "name": str, + "url": str, + "verify_ssl": bool, } @property diff --git a/pipenv/vendor/plette/pipfiles.py b/pipenv/vendor/plette/pipfiles.py index 0113ebeeff..9b01a37400 100644 --- a/pipenv/vendor/plette/pipfiles.py +++ b/pipenv/vendor/plette/pipfiles.py @@ -4,7 +4,7 @@ import pipenv.vendor.tomlkit as tomlkit from .models import ( - DataView, Hash, Requires, PipfileSection, Pipenv, + DataModel, Hash, Requires, PipfileSection, Pipenv, PackageCollection, ScriptCollection, SourceCollection, ) @@ -26,7 +26,7 @@ verify_ssl = true """ -class Pipfile(DataView): +class Pipfile(DataModel): """Representation of a Pipfile. """ __SCHEMA__ = {} @@ -72,7 +72,7 @@ def __getitem__(self, key): return value def __setitem__(self, key, value): - if isinstance(value, DataView): + if isinstance(value, DataModel): self._data[key] = value._data else: self._data[key] = value diff --git a/pipenv/vendor/vendor.txt b/pipenv/vendor/vendor.txt index a672c385b3..37f510fa04 100644 --- a/pipenv/vendor/vendor.txt +++ b/pipenv/vendor/vendor.txt @@ -4,7 +4,7 @@ colorama==0.4.6 dparse==0.6.3 pexpect==4.9.0 pipdeptree==2.16.2 -plette==0.4.4 +plette==2.0.2 ptyprocess==0.7.0 python-dotenv==1.0.1 pythonfinder==2.1.0