From d6182e8247f54f531c0732edef967f9162d27949 Mon Sep 17 00:00:00 2001 From: Oz N Tiram Date: Wed, 30 Aug 2023 10:56:54 +0200 Subject: [PATCH 01/59] Migrate sources to dataclass --- src/plette/models/sources.py | 54 +++++++++++++++--------------------- tests/test_models_sources.py | 42 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 32 deletions(-) create mode 100644 tests/test_models_sources.py diff --git a/src/plette/models/sources.py b/src/plette/models/sources.py index dc2529a..6d6b11d 100644 --- a/src/plette/models/sources.py +++ b/src/plette/models/sources.py @@ -1,45 +1,35 @@ +# pylint: disable=missing-docstring +# pylint: disable=too-few-public-methods + import os -from .base import DataView +from dataclasses import dataclass -class Source(DataView): +@dataclass +class Source: """Information on a "simple" Python package index. This could be PyPI, or a self-hosted index server, etc. The server specified by the `url` attribute is expected to provide the "simple" package API. """ - __SCHEMA__ = { - "name": {"type": "string", "required": True}, - "url": {"type": "string", "required": True}, - "verify_ssl": {"type": "boolean", "required": True}, - } - - @property - def name(self): - return self._data["name"] - - @name.setter - def name(self, value): - self._data["name"] = value - - @property - def url(self): - return self._data["url"] - - @url.setter - def url(self, value): - self._data["url"] = value - - @property - def verify_ssl(self): - return self._data["verify_ssl"] - - @verify_ssl.setter - def verify_ssl(self, value): - self._data["verify_ssl"] = value + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + name: str + url: str + verify_ssl: bool @property def url_expanded(self): - return os.path.expandvars(self._data["url"]) + return os.path.expandvars(self.url) diff --git a/tests/test_models_sources.py b/tests/test_models_sources.py new file mode 100644 index 0000000..0e55b72 --- /dev/null +++ b/tests/test_models_sources.py @@ -0,0 +1,42 @@ + +import hashlib + +import pytest + +from plette.models.sources import NewSource + +def test_source_from_data(): + s = NewSource( + **{ + "name": "devpi", + "url": "https://$USER:$PASS@mydevpi.localhost", + "verify_ssl": False, + } + ) + assert s.name == "devpi" + assert s.url == "https://$USER:$PASS@mydevpi.localhost" + assert s.verify_ssl is False + + +def test_source_as_data_expanded(monkeypatch): + monkeypatch.setattr("os.environ", {"USER": "user", "PASS": "pa55"}) + s = NewSource( + **{ + "name": "devpi", + "url": "https://$USER:$PASS@mydevpi.localhost", + "verify_ssl": False, + } + ) + assert s.url_expanded == "https://user:pa55@mydevpi.localhost" + + +def test_source_as_data_expanded_partial(monkeypatch): + monkeypatch.setattr("os.environ", {"USER": "user"}) + s = NewSource( + **{ + "name": "devpi", + "url": "https://$USER:$PASS@mydevpi.localhost", + "verify_ssl": False, + } + ) + assert s.url_expanded == "https://user:$PASS@mydevpi.localhost" From bf8492e5c314f0b3c89843d953c2a1aad3e57b73 Mon Sep 17 00:00:00 2001 From: Oz N Tiram Date: Fri, 1 Sep 2023 16:16:44 +0200 Subject: [PATCH 02/59] Migrate pacakges to dataclass --- src/plette/models/packages.py | 78 +++++++++++------------------------ tests/test_models_packages.py | 32 ++++++++++++++ 2 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 tests/test_models_packages.py diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index 8b0ebe4..89647e9 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -1,58 +1,30 @@ -from .base import DataView - +from dataclasses import dataclass +from typing import Optional -class Package(DataView): - """A package requirement specified in a Pipfile. - - This is the base class of variants appearing in either `[packages]` or - `[dev-packages]` sections of a Pipfile. - """ - # The extra layer is intentional. Cerberus does not allow top-level keys - # 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"], - }, - } +from .base import DataView - @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, dict): - PackageSpecfiers.validate({"__specifiers__": data}) - def __getattr__(self, key): - if isinstance(self._data, str): - if key == "version": - return self._data - raise AttributeError(key) - try: - return self._data[key] - except KeyError: - pass - raise AttributeError(key) +@dataclass +class PackageSpecfiers: + version: str + extras: list - def __setattr__(self, key, value): - if key == "_data": - super(Package, self).__setattr__(key, value) - elif key == "version" and isinstance(self._data, str): - self._data = value - else: - self._data[key] = value +@dataclass +class Package: -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"}, - } - } - } + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + version: Optional[str] = None + specifiers: Optional[PackageSpecfiers] = None + editable: Optional[bool] = None + extras: Optional[list] = None diff --git a/tests/test_models_packages.py b/tests/test_models_packages.py new file mode 100644 index 0000000..ef8d5de --- /dev/null +++ b/tests/test_models_packages.py @@ -0,0 +1,32 @@ +import pytest + +from plette.models import Package + +def test_package_str(): + p = Package("*") + p.version == "*" + + +def test_package_dict(): + p = Package({"version": "*"}) + p.version == "*" + + +def test_package_wrong_key(): + p = Package({"path": ".", "editable": True}) + assert p.editable is True + with pytest.raises(AttributeError) as ctx: + p.version + assert str(ctx.value) == "version" + + +def test_package_with_wrong_extras(): + with pytest.raises(models.base.ValidationError): + p = Package({"version": "==1.20.0", "extras": "broker"}) + + +def test_package_with_extras(): + p = Package(**{"version": "==1.20.0", "extras": ["broker", "tests"]}) + assert p.extras == ['broker', 'tests'] + + From d21b424e94dedfb72244d39573a0cb3a25744458 Mon Sep 17 00:00:00 2001 From: Oz N Tiram Date: Sat, 2 Sep 2023 16:32:19 +0200 Subject: [PATCH 03/59] Fix model and tests for Package --- src/plette/models/packages.py | 14 ++++++++++---- tests/test_models_packages.py | 23 ++++++++++++----------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index 89647e9..ecc144b 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, List from .base import DataView @dataclass class PackageSpecfiers: - version: str - extras: list + extras: List[str] + @dataclass class Package: @@ -27,4 +27,10 @@ def __post_init__(self): version: Optional[str] = None specifiers: Optional[PackageSpecfiers] = None editable: Optional[bool] = None - extras: Optional[list] = None + extras: Optional[PackageSpecfiers] = None + path: Optional[str] = None + + def validate_extras(self, value, **kwargs): + if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): + raise ValueError("Extras must be a list") + diff --git a/tests/test_models_packages.py b/tests/test_models_packages.py index ef8d5de..d30cca8 100644 --- a/tests/test_models_packages.py +++ b/tests/test_models_packages.py @@ -8,25 +8,26 @@ def test_package_str(): def test_package_dict(): - p = Package({"version": "*"}) + p = Package(**{"version": "*"}) p.version == "*" -def test_package_wrong_key(): - p = Package({"path": ".", "editable": True}) +def test_package_version_is_none(): + p = Package(**{"path": ".", "editable": True}) + assert p.version == None assert p.editable is True - with pytest.raises(AttributeError) as ctx: - p.version - assert str(ctx.value) == "version" - def test_package_with_wrong_extras(): - with pytest.raises(models.base.ValidationError): - p = Package({"version": "==1.20.0", "extras": "broker"}) + with pytest.raises(ValueError): + p = Package(**{"version": "==1.20.0", "extras": "broker"}) + + with pytest.raises(ValueError): + p = Package(**{"version": "==1.20.0", "extras": ["broker", {}]}) + + with pytest.raises(ValueError): + p = Package(**{"version": "==1.20.0", "extras": ["broker", 1]}) def test_package_with_extras(): p = Package(**{"version": "==1.20.0", "extras": ["broker", "tests"]}) assert p.extras == ['broker', 'tests'] - - From 777cbdf6df12ab9cd3097437f0ecbf58a316ac68 Mon Sep 17 00:00:00 2001 From: Oz N Tiram Date: Sat, 2 Sep 2023 18:47:07 +0200 Subject: [PATCH 04/59] Add cli interface to validate Lockfile --- src/plette/__main__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plette/__main__.py b/src/plette/__main__.py index 287e8fb..8242b80 100644 --- a/src/plette/__main__.py +++ b/src/plette/__main__.py @@ -9,10 +9,14 @@ python -m plette -f examples/Pipfile.invalid.list """ -from plette import Pipfile import argparse +import tomlkit + +from plette import Pipfile, Lockfile + + parser = argparse.ArgumentParser() parser.add_argument("-f", "--file", help="Input file") @@ -21,4 +25,8 @@ dest = args.file with open(dest) as f: - pipfile = Pipfile.load(f) + try: + pipfile = Pipfile.load(f) + except tomlkit.exceptions.EmptyKeyError: + f.seek(0) + lockfile = Lockfile.load(f) From bd4c50ca51323547396c036947fc02b8e9521de5 Mon Sep 17 00:00:00 2001 From: Oz N Tiram Date: Sat, 2 Sep 2023 22:53:14 +0200 Subject: [PATCH 05/59] Continue migration to dataclasses --- src/plette/lockfiles.py | 7 ++--- src/plette/models/__init__.py | 3 +- src/plette/models/base.py | 47 +++++++++++++++++++++++++++++++ src/plette/models/hashes.py | 46 ++++++++++++++++++++++++++++++- src/plette/models/sections.py | 52 +++++++++++++++++++++++++++++++++++ tests/test_lockfiles.py | 4 +-- tests/test_models.py | 2 +- 7 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index e8c3091..4463d50 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -3,8 +3,7 @@ import collections.abc as collections_abc - -from .models import DataView, Meta, PackageCollection +from .models import DataView, NewMeta as Meta, PackageCollection class _LockFileEncoder(json.JSONEncoder): @@ -66,9 +65,9 @@ def validate(cls, data): super(Lockfile, cls).validate(data) for key, value in data.items(): if key == "_meta": - Meta.validate(value) + Meta(**{k.replace('-', '_'):v for k,v in value.items()}) else: - PackageCollection.validate(value) + PackageCollection(value) @classmethod def load(cls, f, encoding=None): diff --git a/src/plette/models/__init__.py b/src/plette/models/__init__.py index 20c2899..b00fcd1 100644 --- a/src/plette/models/__init__.py +++ b/src/plette/models/__init__.py @@ -6,7 +6,7 @@ ] from .base import ( - DataView, DataViewCollection, DataViewMapping, DataViewSequence, + DataView, NewDataView, DataViewCollection, DataViewMapping, DataViewSequence, validate, ValidationError, ) @@ -17,6 +17,7 @@ from .sections import ( Meta, + NewMeta, Requires, PackageCollection, Pipenv, diff --git a/src/plette/models/base.py b/src/plette/models/base.py index 72cf372..c8f47e0 100644 --- a/src/plette/models/base.py +++ b/src/plette/models/base.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass try: import cerberus except ImportError: @@ -38,6 +39,52 @@ def validate(cls, data): raise ValidationError(data, v) +class NewModel: + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + +@dataclass +class NewDataView(NewModel): + + _data: dict + + def __repr__(self): + return "{0}({1!r})".format(type(self).__name__, self._data) + + def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError( + "cannot compare {0!r} with {1!r}".format( + type(self).__name__, type(other).__name__ + ) + ) + return self._data == other._data + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + class DataView(object): """A "view" to a data. diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py index d35d312..9c79b17 100644 --- a/src/plette/models/hashes.py +++ b/src/plette/models/hashes.py @@ -1,6 +1,50 @@ from .base import DataView +from dataclasses import dataclass +@dataclass +class NewHash: + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + name: str + value: str + + @classmethod + def from_hash(cls, ins): + """Interpolation to the hash result of `hashlib`. + """ + return cls({ins.name: ins.hexdigest()}) + + @classmethod + def from_line(cls, value): + try: + name, value = value.split(":", 1) + except ValueError: + name = "sha256" + return cls(**{name, value}) + + def __eq__(self, other): + if not isinstance(other, Hash): + raise TypeError("cannot compare Hash with {0!r}".format( + type(other).__name__, + )) + return self._data == other._data + + def as_line(self): + return "{0[0]}:{0[1]}".format(next(iter(self._data.items()))) + + + class Hash(DataView): """A hash. """ @@ -30,7 +74,7 @@ def from_line(cls, value): name, value = value.split(":", 1) except ValueError: name = "sha256" - return cls({name: value}) + return cls(**{name, value}) def __eq__(self, other): if not isinstance(other, Hash): diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index acaaa4d..f3fe4f1 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + from .base import DataView, DataViewMapping, DataViewSequence from .hashes import Hash from .packages import Package @@ -16,6 +18,26 @@ class ScriptCollection(DataViewMapping): class SourceCollection(DataViewSequence): item_class = Source +@dataclass +class SourceCollection: + + sources: list + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_sources(self, value, **kwargs): + for v in value: + Source(**v) class Requires(DataView): """Representation of the `[requires]` section in a Pipfile.""" @@ -61,6 +83,36 @@ class PipfileSection(DataView): def validate(cls, data): pass +@dataclass +class NewMeta: + + hash: dict + pipfile_spec: int + requires: dict + sources: list + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_hash(self, value, **kwargs): + Hash(value) + + def validate_requires(self, value, **kwargs): + Requires(value) + + def validate_sources(self, value, **kwargs): + SourceCollection(value) + + class Meta(DataView): """Representation of the `_meta` section in a Pipfile.lock.""" diff --git a/tests/test_lockfiles.py b/tests/test_lockfiles.py index be41269..92cae2d 100644 --- a/tests/test_lockfiles.py +++ b/tests/test_lockfiles.py @@ -4,7 +4,7 @@ import pytest -from plette import Lockfile, Pipfile +from plette import Lockfile, NewLockfile, Pipfile from plette.models import Package, SourceCollection @@ -36,7 +36,7 @@ def test_lockfile_load(tmpdir): } """, ).replace("____hash____", HASH)) - lock = Lockfile.load(fi) + lock = NewLockfile.load(fi) assert lock.meta.sources == SourceCollection([ { 'url': 'https://pypi.org/simple', diff --git a/tests/test_models.py b/tests/test_models.py index 4587504..d25f8c6 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -229,4 +229,4 @@ def test_validation_error(): error_message = str(exc_info.value) assert "verify_ssl: must be of boolean type" in error_message - assert "url: required field" in error_message + assert "url: required field" in error_messgge From acbb8540f5b1cecaf7807ab0987c56164ca55b7d Mon Sep 17 00:00:00 2001 From: Oz N Tiram Date: Sun, 3 Sep 2023 13:28:44 +0200 Subject: [PATCH 06/59] Update Hash class - use dataclasses --- src/plette/models/hashes.py | 67 ++++++++----------------------------- tests/test_models_hash.py | 20 +++++++++++ 2 files changed, 34 insertions(+), 53 deletions(-) create mode 100644 tests/test_models_hash.py diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py index 9c79b17..8e0f321 100644 --- a/src/plette/models/hashes.py +++ b/src/plette/models/hashes.py @@ -3,7 +3,7 @@ from dataclasses import dataclass @dataclass -class NewHash: +class Hash: def __post_init__(self): """Run validation methods if declared. @@ -18,55 +18,24 @@ def __post_init__(self): setattr(self, name, method(getattr(self, name), field=field)) name: str value: str - - @classmethod - def from_hash(cls, ins): - """Interpolation to the hash result of `hashlib`. - """ - return cls({ins.name: ins.hexdigest()}) - - @classmethod - def from_line(cls, value): - try: - name, value = value.split(":", 1) - except ValueError: - name = "sha256" - return cls(**{name, value}) - - def __eq__(self, other): - if not isinstance(other, Hash): - raise TypeError("cannot compare Hash with {0!r}".format( - type(other).__name__, - )) - return self._data == other._data - - def as_line(self): - return "{0[0]}:{0[1]}".format(next(iter(self._data.items()))) + def validate_name(self, value, **kwargs): + if not isinstance(value, str): + raise ValueError("Hash.name must be a string") - -class Hash(DataView): - """A hash. - """ - __SCHEMA__ = { - "__hash__": { - "type": "list", "minlength": 1, "maxlength": 1, - "schema": { - "type": "list", "minlength": 2, "maxlength": 2, - "schema": {"type": "string"}, - }, - }, - } + return value - @classmethod - def validate(cls, data): - super(Hash, cls).validate({"__hash__": list(data.items())}) + def validate_value(self, value, **kwargs): + if not isinstance(value, str): + raise ValueError("Hash.value must be a string") + + return value @classmethod def from_hash(cls, ins): """Interpolation to the hash result of `hashlib`. """ - return cls({ins.name: ins.hexdigest()}) + return cls(name=ins.name, value=ins.hexdigest()) @classmethod def from_line(cls, value): @@ -74,22 +43,14 @@ def from_line(cls, value): name, value = value.split(":", 1) except ValueError: name = "sha256" - return cls(**{name, value}) + return cls(name, value) def __eq__(self, other): if not isinstance(other, Hash): raise TypeError("cannot compare Hash with {0!r}".format( type(other).__name__, )) - 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.value == other.value def as_line(self): - return "{0[0]}:{0[1]}".format(next(iter(self._data.items()))) + return f"{self.name}:{self.value}" diff --git a/tests/test_models_hash.py b/tests/test_models_hash.py new file mode 100644 index 0000000..7f98bfe --- /dev/null +++ b/tests/test_models_hash.py @@ -0,0 +1,20 @@ +import hashlib + +from plette.models.hashes import Hash + +def test_hash_from_hash(): + v = hashlib.md5(b"foo") + h = Hash.from_hash(v) + assert h.name == "md5" + assert h.value == "acbd18db4cc2f85cedef654fccc4a4d8" + + +def test_hash_from_line(): + h = Hash.from_line("md5:acbd18db4cc2f85cedef654fccc4a4d8") + assert h.name == "md5" + assert h.value == "acbd18db4cc2f85cedef654fccc4a4d8" + + +def test_hash_as_line(): + h = Hash(name="md5", value="acbd18db4cc2f85cedef654fccc4a4d8") + assert h.as_line() == "md5:acbd18db4cc2f85cedef654fccc4a4d8" From c2f63434a06616f3846fc731b8b0eacffd511aa9 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 30 Sep 2023 13:11:18 +0200 Subject: [PATCH 07/59] Fix some linter errors --- src/plette/models/base.py | 2 +- src/plette/models/hashes.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/plette/models/base.py b/src/plette/models/base.py index c8f47e0..aeb8288 100644 --- a/src/plette/models/base.py +++ b/src/plette/models/base.py @@ -84,7 +84,7 @@ def get(self, key, default=None): except KeyError: return default - + class DataView(object): """A "view" to a data. diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py index 8e0f321..0707665 100644 --- a/src/plette/models/hashes.py +++ b/src/plette/models/hashes.py @@ -1,10 +1,11 @@ +from dataclasses import dataclass + from .base import DataView -from dataclasses import dataclass @dataclass class Hash: - + def __post_init__(self): """Run validation methods if declared. The validation method can be a simple check @@ -18,7 +19,7 @@ def __post_init__(self): setattr(self, name, method(getattr(self, name), field=field)) name: str value: str - + def validate_name(self, value, **kwargs): if not isinstance(value, str): raise ValueError("Hash.name must be a string") @@ -47,9 +48,7 @@ def from_line(cls, value): def __eq__(self, other): if not isinstance(other, Hash): - raise TypeError("cannot compare Hash with {0!r}".format( - type(other).__name__, - )) + raise TypeError(f"cannot compare Hash with {type(other).__name__!r}") return self.value == other.value def as_line(self): From eee49895579241fc455343da1c5c425b2f4f7a84 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 30 Sep 2023 13:32:53 +0200 Subject: [PATCH 08/59] Migrate scripts to dataclass --- src/plette/models/packages.py | 2 +- src/plette/models/scripts.py | 52 +++++++++++++++++------------------ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index ecc144b..187c9f0 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -23,7 +23,7 @@ def __post_init__(self): for name, field in self.__dataclass_fields__.items(): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) - + version: Optional[str] = None specifiers: Optional[PackageSpecfiers] = None editable: Optional[bool] = None diff --git a/src/plette/models/scripts.py b/src/plette/models/scripts.py index 69f1c99..b13707a 100644 --- a/src/plette/models/scripts.py +++ b/src/plette/models/scripts.py @@ -1,36 +1,34 @@ import re import shlex + +from dataclasses import dataclass +from typing import List, Union + from .base import DataView +@dataclass(init=False) +class Script: + + def __post_init__(self): + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + script: Union[str, List[str]] + + def __init__(self, script): + + if isinstance(script, str): + script = shlex.split(script) + self._parts = [script[0]] + self._parts.extend(script[1:]) -class Script(DataView): - """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"}, - }, - } - - def __init__(self, data): - super(Script, self).__init__(data) - if isinstance(data, str): - data = shlex.split(data) - self._parts = [data[0]] - self._parts.extend(data[1:]) - - @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}) + def validate_script(self, value, **kwargs): + if not (isinstance(value, str) or \ + (isinstance(value, list) and all(isinstance(i, str) for i in value)) + ): + raise ValueError("script must be a string or a list of strings") def __repr__(self): return "Script({0!r})".format(self._parts) From 38f4b172d90698a326ccf3726d097d3c7260b561 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 19:56:34 +0200 Subject: [PATCH 09/59] Disable some pylint errors, remove unused import --- src/plette/models/hashes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py index 0707665..66bcdb4 100644 --- a/src/plette/models/hashes.py +++ b/src/plette/models/hashes.py @@ -1,7 +1,8 @@ +# pylint: disable=missing-module-docstring,missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member from dataclasses import dataclass -from .base import DataView - @dataclass class Hash: @@ -20,13 +21,13 @@ def __post_init__(self): name: str value: str - def validate_name(self, value, **kwargs): + def validate_name(self, value): if not isinstance(value, str): raise ValueError("Hash.name must be a string") return value - def validate_value(self, value, **kwargs): + def validate_value(self, value): if not isinstance(value, str): raise ValueError("Hash.value must be a string") @@ -40,6 +41,7 @@ def from_hash(cls, ins): @classmethod def from_line(cls, value): + """parse a dependecy line and create a Hash object""" try: name, value = value.split(":", 1) except ValueError: From b4624440c99e7e51eefc997e04666bec9707cf31 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 19:57:58 +0200 Subject: [PATCH 10/59] Disable some pylint errors, remove unused import --- src/plette/models/packages.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index 187c9f0..e891e4d 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -1,13 +1,14 @@ +# pylint: disable=missing-module-docstring,missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member from dataclasses import dataclass from typing import Optional, List -from .base import DataView - @dataclass class PackageSpecfiers: extras: List[str] - + @dataclass class Package: @@ -30,7 +31,6 @@ def __post_init__(self): extras: Optional[PackageSpecfiers] = None path: Optional[str] = None - def validate_extras(self, value, **kwargs): + def validate_extras(self, value): if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): raise ValueError("Extras must be a list") - From 6b8dccf378066831d8433c1a1cb57a25b0555c2f Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 19:59:22 +0200 Subject: [PATCH 11/59] Disable some pylint errors, remove unused import --- src/plette/models/scripts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plette/models/scripts.py b/src/plette/models/scripts.py index b13707a..ffb3e1c 100644 --- a/src/plette/models/scripts.py +++ b/src/plette/models/scripts.py @@ -1,3 +1,6 @@ +# pylint: disable=missing-module-docstring,missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member import re import shlex @@ -5,7 +8,6 @@ from dataclasses import dataclass from typing import List, Union -from .base import DataView @dataclass(init=False) class Script: @@ -24,7 +26,7 @@ def __init__(self, script): self._parts = [script[0]] self._parts.extend(script[1:]) - def validate_script(self, value, **kwargs): + def validate_script(self, value): if not (isinstance(value, str) or \ (isinstance(value, list) and all(isinstance(i, str) for i in value)) ): From c5e2404043b3b010629df47da0e672f1596897ed Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 20:46:27 +0200 Subject: [PATCH 12/59] Add pylintrc --- pylintrc | 632 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 pylintrc diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..fcffcea --- /dev/null +++ b/pylintrc @@ -0,0 +1,632 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +argument-rgx=[a-z_]{2,10} + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + v, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs=[a-z_]{2,10} + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io From b9f25f178d09f833f8c7057c84fa7d3d78233a52 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 20:57:51 +0200 Subject: [PATCH 13/59] Migrate sections.py to dataclasses Signed-off-by: Oz Tiram --- src/plette/models/sections.py | 125 ++++++++++++---------------------- 1 file changed, 44 insertions(+), 81 deletions(-) diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index f3fe4f1..daf8e17 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -1,27 +1,30 @@ +# pylint: disable=missing-module-docstring,missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member + from dataclasses import dataclass +from typing import Optional, List -from .base import DataView, DataViewMapping, DataViewSequence from .hashes import Hash from .packages import Package from .scripts import Script from .sources import Source -class PackageCollection(DataViewMapping): - item_class = Package - +@dataclass +class PackageCollection: + packages: List[Package] -class ScriptCollection(DataViewMapping): - item_class = Script +@dataclass +class ScriptCollection: + scripts: List[Script] -class SourceCollection(DataViewSequence): - item_class = Source @dataclass class SourceCollection: - sources: list + sources: List[Source] def __post_init__(self): """Run validation methods if declared. @@ -35,35 +38,15 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) - def validate_sources(self, value, **kwargs): + def validate_sources(self, value): for v in value: Source(**v) -class Requires(DataView): - """Representation of the `[requires]` section in a Pipfile.""" - - __SCHEMA__ = { - "python_version": { - "type": "string", - }, - "python_full_version": { - "type": "string", - }, - } +@dataclass +class Requires: - @property - def python_version(self): - try: - return self._data["python_version"] - except KeyError: - raise AttributeError("python_version") - - @property - def python_full_version(self): - try: - return self._data["python_full_version"] - except KeyError: - raise AttributeError("python_full_version") + python_version: Optional[str] + python_version: Optional[str] META_SECTIONS = { @@ -72,24 +55,34 @@ def python_full_version(self): "sources": SourceCollection, } -class PipfileSection(DataView): + +@dataclass +class PipfileSection: """ Dummy pipfile validator that needs to be completed in a future PR Hint: many pipfile features are undocumented in pipenv/project.py """ + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) - @classmethod - def validate(cls, data): - pass @dataclass -class NewMeta: +class Meta: - hash: dict - pipfile_spec: int - requires: dict - sources: list + hash: Hash + pipfile_spec: str + requires: Requires + sources: SourceCollection def __post_init__(self): """Run validation methods if declared. @@ -103,44 +96,15 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) - def validate_hash(self, value, **kwargs): - Hash(value) - - def validate_requires(self, value, **kwargs): + def validate_requires(self, value): Requires(value) - def validate_sources(self, value, **kwargs): + def validate_sources(self, value): SourceCollection(value) - -class Meta(DataView): - """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}, - } - - @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) - try: - return META_SECTIONS[key](value) - except KeyError: - return value - - def __setitem__(self, key, value): - if isinstance(value, DataView): - self._data[key] = value._data - else: - self._data[key] = value + def validate_pipfile_spec(self, value): + if int(value) != 6: + raise ValueError('Only pipefile-spec version 6 is supported') @property def hash_(self): @@ -183,9 +147,8 @@ def sources(self, value): self["sources"] = value -class Pipenv(DataView): +@dataclass +class Pipenv: """Represent the [pipenv] section in Pipfile""" - __SCHEMA__ = { - "allow_prereleases": {"type": "boolean", "required": False}, - } + allow_prereleases: Optional[bool] From 100fd3d846a607dbb2e9b10c18ddce2827312a6d Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 20:59:28 +0200 Subject: [PATCH 14/59] WIP: convert lockfiles to dataclasses --- src/plette/lockfiles.py | 60 +++++++++++++++++++++-------------- src/plette/models/__init__.py | 1 - 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index 4463d50..1e49944 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -1,9 +1,15 @@ +# pylint: disable=missing-module-docstring,missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member + import json import numbers import collections.abc as collections_abc -from .models import DataView, NewMeta as Meta, PackageCollection +from dataclasses import dataclass + +from .models import DataView, Meta, PackageCollection class _LockFileEncoder(json.JSONEncoder): @@ -15,19 +21,19 @@ class _LockFileEncoder(json.JSONEncoder): * The output is always UTF-8-encoded text, never binary, even on Python 2. """ def __init__(self): - super(_LockFileEncoder, self).__init__( + super().__init__( indent=4, separators=(",", ": "), sort_keys=True, ) - def encode(self, obj): - content = super(_LockFileEncoder, self).encode(obj) + def encode(self, o): + content = super().encode(o) if not isinstance(content, str): content = content.decode("utf-8") content += "\n" return content - def iterencode(self, obj): - for chunk in super(_LockFileEncoder, self).iterencode(obj): + def iterencode(self, o, _one_shot=False): + for chunk in super().iterencode(o): if not isinstance(chunk, str): chunk = chunk.decode("utf-8") yield chunk @@ -51,30 +57,38 @@ def _copy_jsonsafe(value): return str(value) -class Lockfile(DataView): - """Representation of a Pipfile.lock. - """ +@dataclass +class Lockfile: + """Representation of a Pipfile.lock.""" + + _meta: Meta + default: dict + develop: dict __SCHEMA__ = { "_meta": {"type": "dict", "required": True}, "default": {"type": "dict", "required": True}, "develop": {"type": "dict", "required": True}, } - @classmethod - def validate(cls, data): - super(Lockfile, cls).validate(data) - for key, value in data.items(): - if key == "_meta": - Meta(**{k.replace('-', '_'):v for k,v in value.items()}) - else: - PackageCollection(value) + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) @classmethod - def load(cls, f, encoding=None): + def load(cls, fh, encoding=None): if encoding is None: - data = json.load(f) + data = json.load(fh) else: - data = json.loads(f.read().decode(encoding)) + data = json.loads(fh.read().decode(encoding)) return cls(data) @classmethod @@ -123,14 +137,14 @@ def __setitem__(self, key, value): def is_up_to_date(self, pipfile): return self.meta.hash == pipfile.get_hash() - def dump(self, f, encoding=None): + def dump(self, fh, encoding=None): encoder = _LockFileEncoder() if encoding is None: for chunk in encoder.iterencode(self._data): - f.write(chunk) + fh.write(chunk) else: content = encoder.encode(self._data) - f.write(content.encode(encoding)) + fh.write(content.encode(encoding)) @property def meta(self): diff --git a/src/plette/models/__init__.py b/src/plette/models/__init__.py index b00fcd1..93fd4a5 100644 --- a/src/plette/models/__init__.py +++ b/src/plette/models/__init__.py @@ -17,7 +17,6 @@ from .sections import ( Meta, - NewMeta, Requires, PackageCollection, Pipenv, From 5a0aac24c42b46f1507b83675c2c2ccde8d60e49 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 21:00:20 +0200 Subject: [PATCH 15/59] Add pipfile example with extras in the package spec --- examples/extras-list/Pipfile | 6 + examples/extras-list/Pipfile.lock | 278 ++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 examples/extras-list/Pipfile create mode 100644 examples/extras-list/Pipfile.lock diff --git a/examples/extras-list/Pipfile b/examples/extras-list/Pipfile new file mode 100644 index 0000000..f2b8c73 --- /dev/null +++ b/examples/extras-list/Pipfile @@ -0,0 +1,6 @@ + +# extras should be a list +[packages] +msal = {version="==1.20.0", extras=["broker"]} +parver = '*' + diff --git a/examples/extras-list/Pipfile.lock b/examples/extras-list/Pipfile.lock new file mode 100644 index 0000000..f12a09d --- /dev/null +++ b/examples/extras-list/Pipfile.lock @@ -0,0 +1,278 @@ +{ + "_meta": { + "hash": { + "sha256": "9ed2f9c2327bbff83f6dc015c5dd7d7888c022a1c8d3813ec57461d6def3724d" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "arpeggio": { + "hashes": [ + "sha256:c790b2b06e226d2dd468e4fbfb5b7f506cec66416031fde1441cf1de2a0ba700", + "sha256:f7c8ae4f4056a89e020c24c7202ac8df3e2bc84e416746f20b0da35bb1de0250" + ], + "version": "==2.0.2" + }, + "attrs": { + "hashes": [ + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "certifi": { + "hashes": [ + "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", + "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.7.22" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", + "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", + "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", + "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", + "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", + "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", + "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", + "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", + "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", + "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", + "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", + "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", + "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", + "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", + "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", + "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", + "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", + "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", + "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", + "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", + "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", + "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", + "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", + "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", + "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", + "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", + "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", + "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", + "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", + "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", + "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", + "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", + "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", + "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", + "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", + "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", + "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", + "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", + "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", + "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", + "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", + "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", + "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", + "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", + "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", + "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", + "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", + "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", + "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", + "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", + "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", + "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", + "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", + "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", + "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", + "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", + "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", + "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", + "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", + "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", + "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", + "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", + "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", + "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", + "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", + "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", + "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", + "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", + "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", + "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", + "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", + "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", + "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", + "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", + "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.2.0" + }, + "cryptography": { + "hashes": [ + "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440", + "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288", + "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b", + "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958", + "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b", + "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d", + "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a", + "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404", + "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b", + "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e", + "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2", + "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c", + "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b", + "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9", + "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b", + "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636", + "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99", + "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e", + "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9" + ], + "markers": "python_version >= '3.6'", + "version": "==40.0.2" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "msal": { + "extras": [ + "broker" + ], + "hashes": [ + "sha256:78344cd4c91d6134a593b5e3e45541e666e37b747ff8a6316c3668dd1e6ab6b2", + "sha256:d2f1c26368ecdc28c8657d457352faa0b81b1845a7b889d8676787721ba86792" + ], + "index": "pypi", + "version": "==1.20.0" + }, + "parver": { + "hashes": [ + "sha256:c66d3347a4858643875ef959d8ba7a269d5964bfb690b0dd998b8f39da930be2", + "sha256:d4a3dbb93c53373ee9a0ba055e4858c44169b204b912e49d003ead95db9a9bca" + ], + "index": "pypi", + "version": "==0.4" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pyjwt": { + "extras": [ + "crypto" + ], + "hashes": [ + "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", + "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" + ], + "markers": "python_version >= '3.7'", + "version": "==2.8.0" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "urllib3": { + "hashes": [ + "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", + "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.4" + } + }, + "develop": {} +} From 536fdd421a1253b4e959ec69d4d4b8a742e63888 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 22 Oct 2023 22:14:34 +0200 Subject: [PATCH 16/59] WIP: lockfiles progress --- src/plette/lockfiles.py | 79 +++++++++-------------------------- src/plette/models/sections.py | 49 ++-------------------- src/plette/models/sources.py | 12 ------ tests/test_lockfiles.py | 7 ++-- tests/test_models.py | 11 +---- tests/test_models_sources.py | 13 ++---- 6 files changed, 32 insertions(+), 139 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index 1e49944..e2b5d19 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -7,7 +7,8 @@ import collections.abc as collections_abc -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Optional from .models import DataView, Meta, PackageCollection @@ -60,16 +61,9 @@ def _copy_jsonsafe(value): @dataclass class Lockfile: """Representation of a Pipfile.lock.""" - - _meta: Meta - default: dict - develop: dict - __SCHEMA__ = { - "_meta": {"type": "dict", "required": True}, - "default": {"type": "dict", "required": True}, - "develop": {"type": "dict", "required": True}, - } - + _meta: field(init=False) + default: Optional[dict] = field(default_factory=dict) + develop: Optional[dict] = field(default_factory=dict) def __post_init__(self): """Run validation methods if declared. @@ -83,13 +77,21 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) + self.meta = self._meta + + def validate__meta(self, value, field): + if 'pipfile-spec' in value: + value['pipfile_spec'] = value.pop('pipfile-spec') + + return Meta(**value) + @classmethod def load(cls, fh, encoding=None): if encoding is None: data = json.load(fh) else: data = json.loads(fh.read().decode(encoding)) - return cls(data) + return cls(**data) @classmethod def with_meta_from(cls, pipfile, categories=None): @@ -119,7 +121,7 @@ def with_meta_from(cls, pipfile, categories=None): return cls(data) def __getitem__(self, key): - value = self._data[key] + value = self[key] try: if key == "_meta": return Meta(value) @@ -128,64 +130,23 @@ def __getitem__(self, key): except KeyError: return value - def __setitem__(self, key, value): - if isinstance(value, DataView): - self._data[key] = value._data - else: - self._data[key] = value - def is_up_to_date(self, pipfile): return self.meta.hash == pipfile.get_hash() def dump(self, fh, encoding=None): encoder = _LockFileEncoder() if encoding is None: - for chunk in encoder.iterencode(self._data): + for chunk in encoder.iterencode(self): fh.write(chunk) else: - content = encoder.encode(self._data) + content = encoder.encode(self) fh.write(content.encode(encoding)) + self.meta = self._meta @property def meta(self): - try: - return self["_meta"] - except KeyError: - raise AttributeError("meta") + return self._meta @meta.setter def meta(self, value): - self["_meta"] = value - - @property - def _meta(self): - try: - return self["_meta"] - except KeyError: - raise AttributeError("meta") - - @_meta.setter - def _meta(self, value): - self["_meta"] = value - - @property - def default(self): - try: - return self["default"] - except KeyError: - raise AttributeError("default") - - @default.setter - def default(self, value): - self["default"] = value - - @property - def develop(self): - try: - return self["develop"] - except KeyError: - raise AttributeError("develop") - - @develop.setter - def develop(self, value): - self["develop"] = value + self._meta = value diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index daf8e17..2665395 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -38,7 +38,7 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) - def validate_sources(self, value): + def validate_sources(self, value, field): for v in value: Source(**v) @@ -96,57 +96,16 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) - def validate_requires(self, value): + def validate_requires(self, value, field): Requires(value) - def validate_sources(self, value): + def validate_sources(self, value, field): SourceCollection(value) - def validate_pipfile_spec(self, value): + def validate_pipfile_spec(self, value, field): if int(value) != 6: raise ValueError('Only pipefile-spec version 6 is supported') - @property - def hash_(self): - return self["hash"] - - @hash_.setter - def hash_(self, value): - self["hash"] = value - - @property - def hash(self): - return self["hash"] - - @hash.setter - def hash(self, value): - self["hash"] = value - - @property - def pipfile_spec(self): - return self["pipfile-spec"] - - @pipfile_spec.setter - def pipfile_spec(self, value): - self["pipfile-spec"] = value - - @property - def requires(self): - return self["requires"] - - @requires.setter - def requires(self, value): - self["requires"] = value - - @property - def sources(self): - return self["sources"] - - @sources.setter - def sources(self, value): - self["sources"] = value - - @dataclass class Pipenv: """Represent the [pipenv] section in Pipfile""" diff --git a/src/plette/models/sources.py b/src/plette/models/sources.py index 6d6b11d..fe6a9ff 100644 --- a/src/plette/models/sources.py +++ b/src/plette/models/sources.py @@ -14,18 +14,6 @@ class Source: specified by the `url` attribute is expected to provide the "simple" package API. """ - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - name: str url: str verify_ssl: bool diff --git a/tests/test_lockfiles.py b/tests/test_lockfiles.py index 92cae2d..db502c3 100644 --- a/tests/test_lockfiles.py +++ b/tests/test_lockfiles.py @@ -1,10 +1,8 @@ -from __future__ import unicode_literals - import textwrap import pytest -from plette import Lockfile, NewLockfile, Pipfile +from plette import Lockfile, Pipfile from plette.models import Package, SourceCollection @@ -36,7 +34,8 @@ def test_lockfile_load(tmpdir): } """, ).replace("____hash____", HASH)) - lock = NewLockfile.load(fi) + lock = Lockfile.load(fi) + import pdb; pdb.set_trace() assert lock.meta.sources == SourceCollection([ { 'url': 'https://pypi.org/simple', diff --git a/tests/test_models.py b/tests/test_models.py index d25f8c6..5a93ba8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,11 +2,6 @@ import pytest -try: - import cerberus -except ImportError: - cerberus = None - from plette import models @@ -89,7 +84,6 @@ def test_requires_python_full_version_no_version(): assert str(ctx.value) == "python_version" -@pytest.mark.skipif(cerberus is None, reason="Skip validation without Ceberus") def test_allows_python_version_and_full(): r = models.Requires({"python_version": "8.1", "python_full_version": "8.1.9"}) assert r.python_version == "8.1" @@ -114,13 +108,11 @@ def test_package_wrong_key(): assert str(ctx.value) == "version" -@pytest.mark.skipif(cerberus is None, reason="Skip validation without Ceberus") def test_package_with_wrong_extras(): with pytest.raises(models.base.ValidationError): p = models.Package({"version": "==1.20.0", "extras": "broker"}) -@pytest.mark.skipif(cerberus is None, reason="Skip validation without Ceberus") def test_package_with_extras(): p = models.Package({"version": "==1.20.0", "extras": ["broker", "tests"]}) assert p.extras == ['broker', 'tests'] @@ -191,7 +183,7 @@ def test_set_slice(sources): "verify_ssl": True, }, ] - assert sources._data == [ + assert sources == [ { "name": "pypi", "url": "https://pypi.org/simple", @@ -221,7 +213,6 @@ def test_del_slice(sources): ] -@pytest.mark.skipif(cerberus is None, reason="Skip validation without Ceberus") def test_validation_error(): data = {"name": "test", "verify_ssl": 1} with pytest.raises(models.base.ValidationError) as exc_info: diff --git a/tests/test_models_sources.py b/tests/test_models_sources.py index 0e55b72..7e3e0a5 100644 --- a/tests/test_models_sources.py +++ b/tests/test_models_sources.py @@ -1,12 +1,7 @@ - -import hashlib - -import pytest - -from plette.models.sources import NewSource +from plette.models.sources import Source def test_source_from_data(): - s = NewSource( + s = Source( **{ "name": "devpi", "url": "https://$USER:$PASS@mydevpi.localhost", @@ -20,7 +15,7 @@ def test_source_from_data(): def test_source_as_data_expanded(monkeypatch): monkeypatch.setattr("os.environ", {"USER": "user", "PASS": "pa55"}) - s = NewSource( + s = Source( **{ "name": "devpi", "url": "https://$USER:$PASS@mydevpi.localhost", @@ -32,7 +27,7 @@ def test_source_as_data_expanded(monkeypatch): def test_source_as_data_expanded_partial(monkeypatch): monkeypatch.setattr("os.environ", {"USER": "user"}) - s = NewSource( + s = Source( **{ "name": "devpi", "url": "https://$USER:$PASS@mydevpi.localhost", From b8b23b7d8e1035221143ccbe36a64145b4336703 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Mon, 6 Nov 2023 13:47:16 +0100 Subject: [PATCH 17/59] Add missing return value in SourceCollection validator --- src/plette/models/sections.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index 2665395..32f12d8 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -41,6 +41,7 @@ def __post_init__(self): def validate_sources(self, value, field): for v in value: Source(**v) + return value @dataclass class Requires: From aff82f7c68b0913ec1b2183e46c233c43de9d7cf Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Mon, 6 Nov 2023 14:05:00 +0100 Subject: [PATCH 18/59] Continue work on lockfile parsing --- src/plette/lockfiles.py | 5 ++++- src/plette/models/sections.py | 6 ++++-- tests/test_lockfiles.py | 33 +++++++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index e2b5d19..73b0142 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -10,7 +10,7 @@ from dataclasses import dataclass, field from typing import Optional -from .models import DataView, Meta, PackageCollection +from .models import Meta, PackageCollection class _LockFileEncoder(json.JSONEncoder): @@ -80,6 +80,9 @@ def __post_init__(self): self.meta = self._meta def validate__meta(self, value, field): + return self.validate_meta(value, field) + + def validate_meta(self, value, field): if 'pipfile-spec' in value: value['pipfile_spec'] = value.pop('pipfile-spec') diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index 32f12d8..bfd7107 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -98,14 +98,16 @@ def __post_init__(self): setattr(self, name, method(getattr(self, name), field=field)) def validate_requires(self, value, field): - Requires(value) + return Requires(value) def validate_sources(self, value, field): - SourceCollection(value) + return SourceCollection(value) def validate_pipfile_spec(self, value, field): if int(value) != 6: raise ValueError('Only pipefile-spec version 6 is supported') + return value + @dataclass class Pipenv: diff --git a/tests/test_lockfiles.py b/tests/test_lockfiles.py index db502c3..7b11a24 100644 --- a/tests/test_lockfiles.py +++ b/tests/test_lockfiles.py @@ -1,7 +1,5 @@ import textwrap -import pytest - from plette import Lockfile, Pipfile from plette.models import Package, SourceCollection @@ -9,7 +7,7 @@ HASH = "9aaf3dbaf8c4df3accd4606eb2275d3b91c9db41be4fd5a97ecc95d79a12cfe6" -def test_lockfile_load(tmpdir): +def test_lockfile_load_sources(tmpdir): fi = tmpdir.join("in.json") fi.write(textwrap.dedent( """\ @@ -35,7 +33,6 @@ def test_lockfile_load(tmpdir): """, ).replace("____hash____", HASH)) lock = Lockfile.load(fi) - import pdb; pdb.set_trace() assert lock.meta.sources == SourceCollection([ { 'url': 'https://pypi.org/simple', @@ -43,6 +40,34 @@ def test_lockfile_load(tmpdir): 'name': 'pypi', }, ]) + + +def test_lockfile_load_sources_package_spec(tmpdir): + fi = tmpdir.join("in.json") + fi.write(textwrap.dedent( + """\ + { + "_meta": { + "hash": {"sha256": "____hash____"}, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "flask": {"version": "*"}, + "jinja2": "*" + }, + "develop": {} + } + """, + ).replace("____hash____", HASH)) + lock = Lockfile.load(fi) assert lock.default["jinja2"] == Package("*") From e8f8b13030cef2696472ef75fe628acae33d1ddb Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Mon, 6 Nov 2023 14:08:01 +0100 Subject: [PATCH 19/59] Fix hasheh.py --- src/plette/models/hashes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py index 66bcdb4..d007260 100644 --- a/src/plette/models/hashes.py +++ b/src/plette/models/hashes.py @@ -21,13 +21,13 @@ def __post_init__(self): name: str value: str - def validate_name(self, value): + def validate_name(self, value, field): if not isinstance(value, str): raise ValueError("Hash.name must be a string") return value - def validate_value(self, value): + def validate_value(self, value, field): if not isinstance(value, str): raise ValueError("Hash.value must be a string") From 6d6895b1a8728f495ed344a9cf6eeae21b313312 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Mon, 6 Nov 2023 14:18:32 +0100 Subject: [PATCH 20/59] Fix tests for packages.py --- src/plette/models/packages.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index e891e4d..656728f 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -31,6 +31,9 @@ def __post_init__(self): extras: Optional[PackageSpecfiers] = None path: Optional[str] = None - def validate_extras(self, value): + def validate_extras(self, value, field): + if value is None: + return value if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): - raise ValueError("Extras must be a list") + raise ValueError("Extras must be a list or None") + return value From 91d322ddcb3c89148f1291dafa99cde5f5c9d63d Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 11 Nov 2023 11:20:23 +0100 Subject: [PATCH 21/59] Update pylint rules --- pylintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index fcffcea..b260b33 100644 --- a/pylintrc +++ b/pylintrc @@ -191,7 +191,8 @@ good-names=i, v, ex, Run, - _ + _, + fi # Good variable names regexes, separated by a comma. If names match any regex, # they will always be accepted From 31e4466df750695287d42016df086cd823662457 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 11 Nov 2023 11:20:56 +0100 Subject: [PATCH 22/59] Fix another lockfiles test Validate Packages properly in the default section --- src/plette/lockfiles.py | 14 +++++++++++++- tests/test_lockfiles.py | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index 73b0142..ca0845c 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -10,7 +10,7 @@ from dataclasses import dataclass, field from typing import Optional -from .models import Meta, PackageCollection +from .models import Meta, PackageCollection, Package class _LockFileEncoder(json.JSONEncoder): @@ -73,6 +73,7 @@ def __post_init__(self): The validation is performed by calling a function named: `validate_(self, value, field) -> field.type` """ + for name, field in self.__dataclass_fields__.items(): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) @@ -88,6 +89,17 @@ def validate_meta(self, value, field): return Meta(**value) + def validate_default(self, value, field): + packages = {} + for name, spec in value.items(): + if isinstance(spec, str): + packages[name] = Package(spec) + else: + packages[name] = Package(**spec) + + return packages + + @classmethod def load(cls, fh, encoding=None): if encoding is None: diff --git a/tests/test_lockfiles.py b/tests/test_lockfiles.py index 7b11a24..dcf3c15 100644 --- a/tests/test_lockfiles.py +++ b/tests/test_lockfiles.py @@ -1,3 +1,6 @@ +# pylint: disable=missing-module-docstring,missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member import textwrap from plette import Lockfile, Pipfile From 229dcff7bb032e985b2e1d53cfb92228081a233f Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 11 Nov 2023 11:24:37 +0100 Subject: [PATCH 23/59] Remove tests for sources from test_models.py These tests are now found in tests/test_models_sources.py --- tests/test_models.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 5a93ba8..1f57abc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -23,43 +23,6 @@ def test_hash_as_line(): assert h.as_line() == "md5:acbd18db4cc2f85cedef654fccc4a4d8" -def test_source_from_data(): - s = models.Source( - { - "name": "devpi", - "url": "https://$USER:$PASS@mydevpi.localhost", - "verify_ssl": False, - } - ) - assert s.name == "devpi" - assert s.url == "https://$USER:$PASS@mydevpi.localhost" - assert s.verify_ssl is False - - -def test_source_as_data_expanded(monkeypatch): - monkeypatch.setattr("os.environ", {"USER": "user", "PASS": "pa55"}) - s = models.Source( - { - "name": "devpi", - "url": "https://$USER:$PASS@mydevpi.localhost", - "verify_ssl": False, - } - ) - assert s.url_expanded == "https://user:pa55@mydevpi.localhost" - - -def test_source_as_data_expanded_partial(monkeypatch): - monkeypatch.setattr("os.environ", {"USER": "user"}) - s = models.Source( - { - "name": "devpi", - "url": "https://$USER:$PASS@mydevpi.localhost", - "verify_ssl": False, - } - ) - assert s.url_expanded == "https://user:$PASS@mydevpi.localhost" - - def test_requires_python_version(): r = models.Requires({"python_version": "8.19"}) assert r.python_version == "8.19" From bed6cba9d39a72068bd1105ccbdf1f01bb05fcf9 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 11 Nov 2023 11:30:18 +0100 Subject: [PATCH 24/59] Move all tests for package into tests/test_models_packages.py --- tests/test_models.py | 28 ---------------------------- tests/test_models_packages.py | 6 ++++++ 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 1f57abc..a59fa0f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -53,34 +53,6 @@ def test_allows_python_version_and_full(): assert r.python_full_version == "8.1.9" -def test_package_str(): - p = models.Package("*") - p.version == "*" - - -def test_package_dict(): - p = models.Package({"version": "*"}) - p.version == "*" - - -def test_package_wrong_key(): - p = models.Package({"path": ".", "editable": True}) - assert p.editable is True - with pytest.raises(AttributeError) as ctx: - p.version - assert str(ctx.value) == "version" - - -def test_package_with_wrong_extras(): - with pytest.raises(models.base.ValidationError): - p = models.Package({"version": "==1.20.0", "extras": "broker"}) - - -def test_package_with_extras(): - p = models.Package({"version": "==1.20.0", "extras": ["broker", "tests"]}) - assert p.extras == ['broker', 'tests'] - - HASH = "9aaf3dbaf8c4df3accd4606eb2275d3b91c9db41be4fd5a97ecc95d79a12cfe6" diff --git a/tests/test_models_packages.py b/tests/test_models_packages.py index d30cca8..8a81446 100644 --- a/tests/test_models_packages.py +++ b/tests/test_models_packages.py @@ -31,3 +31,9 @@ def test_package_with_wrong_extras(): def test_package_with_extras(): p = Package(**{"version": "==1.20.0", "extras": ["broker", "tests"]}) assert p.extras == ['broker', 'tests'] + + +def test_package_wrong_key(): + p = Package(**{"path": ".", "editable": True}) + assert p.editable is True + assert p.version is None From faebcee901944c5e5457073e0d920ee3f28b5995 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 11 Nov 2023 21:29:58 +0100 Subject: [PATCH 25/59] Extract tests for meta These are now found tests/test_models_meta.py. Also fixes for the class Meta as required for the tests to pass. --- src/plette/models/hashes.py | 4 ++-- src/plette/models/sections.py | 10 ++++++++++ tests/test_models.py | 21 --------------------- tests/test_models_meta.py | 20 ++++++++++++++++++++ 4 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 tests/test_models_meta.py diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py index d007260..98f4dd3 100644 --- a/src/plette/models/hashes.py +++ b/src/plette/models/hashes.py @@ -44,8 +44,8 @@ def from_line(cls, value): """parse a dependecy line and create a Hash object""" try: name, value = value.split(":", 1) - except ValueError: - name = "sha256" + except AttributeError: + name, value = list(value.items())[0] return cls(name, value) def __eq__(self, other): diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index bfd7107..97c0e67 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -85,6 +85,10 @@ class Meta: requires: Requires sources: SourceCollection + @classmethod + def from_dict(cls, d: dict) -> "Meta": + return cls(**{k.replace('-', '_'): v for k, v in d.items()}) + def __post_init__(self): """Run validation methods if declared. The validation method can be a simple check @@ -97,6 +101,12 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) + def validate_hash(self, value, field): + try: + return Hash(**value) + except TypeError: + return Hash.from_line(value) + def validate_requires(self, value, field): return Requires(value) diff --git a/tests/test_models.py b/tests/test_models.py index a59fa0f..ea0ffe1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -53,27 +53,6 @@ def test_allows_python_version_and_full(): assert r.python_full_version == "8.1.9" -HASH = "9aaf3dbaf8c4df3accd4606eb2275d3b91c9db41be4fd5a97ecc95d79a12cfe6" - - -def test_meta(): - m = models.Meta( - { - "hash": {"sha256": HASH}, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": True, - }, - ], - } - ) - assert m.hash.name == "sha256" - - @pytest.fixture() def sources(): return models.SourceCollection( diff --git a/tests/test_models_meta.py b/tests/test_models_meta.py new file mode 100644 index 0000000..20f9631 --- /dev/null +++ b/tests/test_models_meta.py @@ -0,0 +1,20 @@ +from plette.models import Meta + +HASH = "9aaf3dbaf8c4df3accd4606eb2275d3b91c9db41be4fd5a97ecc95d79a12cfe6" + +def test_meta(): + m = Meta.from_dict( + { + "hash": {"sha256": HASH}, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": True, + }, + ], + } + ) + assert m.hash.name == "sha256" From f84d4015aabeb93938ea87e02033d65430747c50 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 11 Nov 2023 21:54:17 +0100 Subject: [PATCH 26/59] Extract tests for hashes Fix the Hash class so the tests pass again. --- src/plette/models/hashes.py | 9 +++++++++ tests/test_models.py | 18 ------------------ tests/test_models_hash.py | 2 +- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py index 98f4dd3..016f9a3 100644 --- a/src/plette/models/hashes.py +++ b/src/plette/models/hashes.py @@ -39,6 +39,15 @@ def from_hash(cls, ins): """ return cls(name=ins.name, value=ins.hexdigest()) + @classmethod + def from_dict(cls, value): + """parse a depedency line and create an Hash object""" + try: + name, value = list(value.items())[0] + except AttributeError: + name, value = value.split(":", 1) + return cls(name, value) + @classmethod def from_line(cls, value): """parse a dependecy line and create a Hash object""" diff --git a/tests/test_models.py b/tests/test_models.py index ea0ffe1..86202be 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,24 +5,6 @@ from plette import models -def test_hash_from_hash(): - v = hashlib.md5(b"foo") - h = models.Hash.from_hash(v) - assert h.name == "md5" - assert h.value == "acbd18db4cc2f85cedef654fccc4a4d8" - - -def test_hash_from_line(): - h = models.Hash.from_line("md5:acbd18db4cc2f85cedef654fccc4a4d8") - assert h.name == "md5" - assert h.value == "acbd18db4cc2f85cedef654fccc4a4d8" - - -def test_hash_as_line(): - h = models.Hash({"md5": "acbd18db4cc2f85cedef654fccc4a4d8"}) - assert h.as_line() == "md5:acbd18db4cc2f85cedef654fccc4a4d8" - - def test_requires_python_version(): r = models.Requires({"python_version": "8.19"}) assert r.python_version == "8.19" diff --git a/tests/test_models_hash.py b/tests/test_models_hash.py index 7f98bfe..9a14131 100644 --- a/tests/test_models_hash.py +++ b/tests/test_models_hash.py @@ -16,5 +16,5 @@ def test_hash_from_line(): def test_hash_as_line(): - h = Hash(name="md5", value="acbd18db4cc2f85cedef654fccc4a4d8") + h = Hash.from_dict({"md5": "acbd18db4cc2f85cedef654fccc4a4d8"}) assert h.as_line() == "md5:acbd18db4cc2f85cedef654fccc4a4d8" From 56fa7d699185b975b44b7e6a79456bb0fdf32087 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 12 Nov 2023 08:48:32 +0100 Subject: [PATCH 27/59] Extract tests for models.Requires Also fix the class and tests. --- src/plette/models/sections.py | 4 ++-- tests/test_models.py | 30 ------------------------------ tests/test_models_requires.py | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 32 deletions(-) create mode 100644 tests/test_models_requires.py diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index 97c0e67..f5524e1 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -45,9 +45,9 @@ def validate_sources(self, value, field): @dataclass class Requires: + python_version: Optional[str] = None + python_full_version: Optional[str] = None - python_version: Optional[str] - python_version: Optional[str] META_SECTIONS = { diff --git a/tests/test_models.py b/tests/test_models.py index 86202be..be134f9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,36 +5,6 @@ from plette import models -def test_requires_python_version(): - r = models.Requires({"python_version": "8.19"}) - assert r.python_version == "8.19" - - -def test_requires_python_version_no_full_version(): - r = models.Requires({"python_version": "8.19"}) - with pytest.raises(AttributeError) as ctx: - r.python_full_version - assert str(ctx.value) == "python_full_version" - - -def test_requires_python_full_version(): - r = models.Requires({"python_full_version": "8.19"}) - assert r.python_full_version == "8.19" - - -def test_requires_python_full_version_no_version(): - r = models.Requires({"python_full_version": "8.19"}) - with pytest.raises(AttributeError) as ctx: - r.python_version - assert str(ctx.value) == "python_version" - - -def test_allows_python_version_and_full(): - r = models.Requires({"python_version": "8.1", "python_full_version": "8.1.9"}) - assert r.python_version == "8.1" - assert r.python_full_version == "8.1.9" - - @pytest.fixture() def sources(): return models.SourceCollection( diff --git a/tests/test_models_requires.py b/tests/test_models_requires.py new file mode 100644 index 0000000..814b7a4 --- /dev/null +++ b/tests/test_models_requires.py @@ -0,0 +1,27 @@ +import pytest +from plette import models + +def test_requires_python_version(): + r = models.Requires(**{"python_version": "8.19"}) + assert r.python_version == "8.19" + + +def test_requires_python_version_no_full_version(): + r = models.Requires(**{"python_version": "8.19"}) + r.python_full_version is None + + +def test_requires_python_full_version(): + r = models.Requires(**{"python_full_version": "8.19"}) + assert r.python_full_version == "8.19" + + +def test_requires_python_full_version_no_version(): + r = models.Requires(**{"python_full_version": "8.19"}) + r.python_version is None + + +def test_allows_python_version_and_full(): + r = models.Requires(**{"python_version": "8.1", "python_full_version": "8.1.9"}) + assert r.python_version == "8.1" + assert r.python_full_version == "8.1.9" From e8aa17b8348e60798269f6cee9979ad2280fe5db Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 18 Nov 2023 12:53:54 +0100 Subject: [PATCH 28/59] Restore iterable behaviour for SourceCollection --- src/plette/models/sections.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index f5524e1..ec4ef03 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -39,9 +39,30 @@ def __post_init__(self): setattr(self, name, method(getattr(self, name), field=field)) def validate_sources(self, value, field): + sources = [] for v in value: - Source(**v) - return value + if isinstance(v, dict): + sources.append(Source(**v)) + elif isinstance(v, Source): + sources.append(v) + return sources + + def __iter__(self): + return (d for d in self.sources) + + def __getitem__(self, key): + if isinstance(key, slice): + return SourceCollection(self.sources[key]) + if isinstance(key, int): + src = self.sources[key] + if isinstance(src, dict): + return Source(**key) + if isinstance(src, Source): + return src + raise TypeError(f"Unextepcted type {type(src)}") + + def __len__(self): + return len(self.sources) @dataclass class Requires: From 09859c660da71acd3b655cc817487cf45961223f Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 18 Nov 2023 13:12:53 +0100 Subject: [PATCH 29/59] Fix tests for models.SourceCollection --- src/plette/models/sections.py | 14 +++++ tests/test_models_sourcecollections.py | 81 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/test_models_sourcecollections.py diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index ec4ef03..895a1fc 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -64,6 +64,20 @@ def __getitem__(self, key): def __len__(self): return len(self.sources) + def __setitem__(self, key, value): + if isinstance(key, slice): + self.sources[key] = value + elif isinstance(value, Source): + self.sources.append(value) + elif isinstance(value, list): + self.sources.extend(value) + else: + raise TypeError(f"Unextepcted type {type(value)} for {value}") + + def __delitem__(self, key): + del self.sources[key] + + @dataclass class Requires: python_version: Optional[str] = None diff --git a/tests/test_models_sourcecollections.py b/tests/test_models_sourcecollections.py new file mode 100644 index 0000000..54165c9 --- /dev/null +++ b/tests/test_models_sourcecollections.py @@ -0,0 +1,81 @@ +import hashlib + +import pytest + +from plette import models +from plette.models import Source, SourceCollection + + +@pytest.fixture() +def sources(): + return models.SourceCollection( + [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": True, + }, + { + "name": "devpi", + "url": "http://127.0.0.1:$DEVPI_PORT/simple", + "verify_ssl": True, + }, + ] + ) + + +def test_get_slice(sources): + sliced = sources[:1] + assert isinstance(sliced, models.SourceCollection) + assert len(sliced) == 1 + assert sliced[0] == models.Source( + **{ + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": True, + } + ) + + +def test_set_slice(sources): + sources[1:] = [ + Source(**{ + "name": "localpi-4433", + "url": "https://127.0.0.1:4433/simple", + "verify_ssl": False, + }), + Source(**{ + "name": "localpi-8000", + "url": "http://127.0.0.1:8000/simple", + "verify_ssl": True, + }), + ] + assert sources == \ + SourceCollection([ + Source(**{ + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": True, + }), + Source(**{ + "name": "localpi-4433", + "url": "https://127.0.0.1:4433/simple", + "verify_ssl": False, + }), + Source(**{ + "name": "localpi-8000", + "url": "http://127.0.0.1:8000/simple", + "verify_ssl": True, + }), + ]) + + +def test_del_slice(sources): + del sources[:1] + assert sources == SourceCollection([ + Source(**{ + "name": "devpi", + "url": "http://127.0.0.1:$DEVPI_PORT/simple", + "verify_ssl": True, + }), + ]) From 37617ce22031269e95467d47210b20d1d09d9369 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 18 Nov 2023 13:14:03 +0100 Subject: [PATCH 30/59] Remove tests for SourceCollection from test_models These tests are now in tests/test_models_sourcecollection.py --- tests/test_models.py | 75 -------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index be134f9..2e30784 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -4,81 +4,6 @@ from plette import models - -@pytest.fixture() -def sources(): - return models.SourceCollection( - [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": True, - }, - { - "name": "devpi", - "url": "http://127.0.0.1:$DEVPI_PORT/simple", - "verify_ssl": True, - }, - ] - ) - - -def test_get_slice(sources): - sliced = sources[:1] - assert isinstance(sliced, models.SourceCollection) - assert len(sliced) == 1 - assert sliced[0] == models.Source( - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": True, - } - ) - - -def test_set_slice(sources): - sources[1:] = [ - { - "name": "localpi-4433", - "url": "https://127.0.0.1:4433/simple", - "verify_ssl": False, - }, - { - "name": "localpi-8000", - "url": "http://127.0.0.1:8000/simple", - "verify_ssl": True, - }, - ] - assert sources == [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": True, - }, - { - "name": "localpi-4433", - "url": "https://127.0.0.1:4433/simple", - "verify_ssl": False, - }, - { - "name": "localpi-8000", - "url": "http://127.0.0.1:8000/simple", - "verify_ssl": True, - }, - ] - - -def test_del_slice(sources): - del sources[:1] - assert sources._data == [ - { - "name": "devpi", - "url": "http://127.0.0.1:$DEVPI_PORT/simple", - "verify_ssl": True, - }, - ] - - def test_validation_error(): data = {"name": "test", "verify_ssl": 1} with pytest.raises(models.base.ValidationError) as exc_info: From 00eac23724da650e56d1b80f5b7fbcaa8357f1ab Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 18 Nov 2023 13:21:54 +0100 Subject: [PATCH 31/59] Remove cerberus from test_scripts.py --- tests/test_scripts.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 0965469..1087a50 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -1,10 +1,5 @@ import pytest -try: - import cerberus -except ImportError: - cerberus = None - from plette.models import Script @@ -14,13 +9,9 @@ def test_parse(): assert script.args == ['-c', "print('hello')"], script -@pytest.mark.skipif(cerberus is None, reason="Skip validation without Ceberus") def test_parse_error(): - with pytest.raises(ValueError) as ctx: + with pytest.raises(IndexError): Script('') - assert cerberus.errors.EMPTY_NOT_ALLOWED in ctx.value.validator._errors - assert len(ctx.value.validator._errors) == 1 - def test_cmdify(): script = Script(['python', '-c', "print('hello world')"]) From b49bb3a5caa905a615d0e89896189443248173ea Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 19 Nov 2023 11:19:59 +0100 Subject: [PATCH 32/59] Fix test for validation error --- src/plette/models/base.py | 16 +--------------- src/plette/models/sources.py | 24 +++++++++++++++++++++++- tests/test_models.py | 14 -------------- tests/test_models_sources.py | 21 +++++++++++++++++++++ 4 files changed, 45 insertions(+), 30 deletions(-) delete mode 100644 tests/test_models.py diff --git a/src/plette/models/base.py b/src/plette/models/base.py index aeb8288..e71fd12 100644 --- a/src/plette/models/base.py +++ b/src/plette/models/base.py @@ -6,21 +6,7 @@ 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 - ) - ) - + pass VALIDATORS = {} diff --git a/src/plette/models/sources.py b/src/plette/models/sources.py index fe6a9ff..c6fa45d 100644 --- a/src/plette/models/sources.py +++ b/src/plette/models/sources.py @@ -3,7 +3,11 @@ import os + from dataclasses import dataclass +from typing import Optional + +from .base import ValidationError @dataclass @@ -15,9 +19,27 @@ class Source: package API. """ name: str - url: str verify_ssl: bool + #url: Optional[str] = None + url: str @property def url_expanded(self): return os.path.expandvars(self.url) + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_verify_ssl(self, value, field): + if not isinstance(value, bool): + raise ValidationError(f"{field.name}: must be of boolean type") + return value diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 2e30784..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,14 +0,0 @@ -import hashlib - -import pytest - -from plette import models - -def test_validation_error(): - data = {"name": "test", "verify_ssl": 1} - with pytest.raises(models.base.ValidationError) as exc_info: - models.Source.validate(data) - - error_message = str(exc_info.value) - assert "verify_ssl: must be of boolean type" in error_message - assert "url: required field" in error_messgge diff --git a/tests/test_models_sources.py b/tests/test_models_sources.py index 7e3e0a5..0a03f2a 100644 --- a/tests/test_models_sources.py +++ b/tests/test_models_sources.py @@ -35,3 +35,24 @@ def test_source_as_data_expanded_partial(monkeypatch): } ) assert s.url_expanded == "https://user:$PASS@mydevpi.localhost" + + +def test_validation_error(): + data = {"name": "test", "verify_ssl": 1} + + with pytest.raises(TypeError) as exc_info: + Source(**data) + + error_message = str(exc_info.value) + + assert "missing 1 required positional argument: 'url'" in error_message + + data["url"] = "http://localhost:8000" + + with pytest.raises(models.base.ValidationError) as exc_info: + Source(**data) + + error_message = str(exc_info.value) + + + assert "verify_ssl: must be of boolean type" in error_message From 9eccc5496d64c13f6a5eb2d74c3721c33e79727f Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 19 Nov 2023 12:18:09 +0100 Subject: [PATCH 33/59] WIP: slowly getting lockfiles properly work ... JSON encoding is working, but the test still fails. --- src/plette/lockfiles.py | 60 ++++++++++------------------------- src/plette/pipfiles.py | 69 +++++++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 73 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index ca0845c..a6b1608 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -13,33 +13,13 @@ from .models import Meta, PackageCollection, Package -class _LockFileEncoder(json.JSONEncoder): - """A specilized JSON encoder to convert loaded data into a lock file. - - This adds a few characteristics to the encoder: - - * The JSON is always prettified with indents and spaces. - * The output is always UTF-8-encoded text, never binary, even on Python 2. - """ - def __init__(self): - super().__init__( - indent=4, separators=(",", ": "), sort_keys=True, - ) - - def encode(self, o): - content = super().encode(o) - if not isinstance(content, str): - content = content.decode("utf-8") - content += "\n" - return content - - def iterencode(self, o, _one_shot=False): - for chunk in super().iterencode(o): - if not isinstance(chunk, str): - chunk = chunk.decode("utf-8") - yield chunk - yield "\n" +import dataclasses, json +class DCJSONEncoder(json.JSONEncoder): + def default(self, o): + if dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + return super().default(o) PIPFILE_SPEC_CURRENT = 6 @@ -84,9 +64,10 @@ def validate__meta(self, value, field): return self.validate_meta(value, field) def validate_meta(self, value, field): + if "_meta" in value: + value = value["_meta"] if 'pipfile-spec' in value: value['pipfile_spec'] = value.pop('pipfile-spec') - return Meta(**value) def validate_default(self, value, field): @@ -99,7 +80,6 @@ def validate_default(self, value, field): return packages - @classmethod def load(cls, fh, encoding=None): if encoding is None: @@ -112,23 +92,23 @@ def load(cls, fh, encoding=None): def with_meta_from(cls, pipfile, categories=None): data = { "_meta": { - "hash": _copy_jsonsafe(pipfile.get_hash()._data), + "hash": _copy_jsonsafe(pipfile.get_hash()), "pipfile-spec": PIPFILE_SPEC_CURRENT, - "requires": _copy_jsonsafe(pipfile._data.get("requires", {})), - "sources": _copy_jsonsafe(pipfile.sources._data), + "requires": _copy_jsonsafe(getattr(pipfile, "requires", {})), + "sources": _copy_jsonsafe(pipfile.sources), }, } if categories is None: - data["default"] = _copy_jsonsafe(pipfile._data.get("packages", {})) - data["develop"] = _copy_jsonsafe(pipfile._data.get("dev-packages", {})) + data["default"] = _copy_jsonsafe(getattr(pipfile, "packages", {})) + data["develop"] = _copy_jsonsafe(getattr(pipfile, "dev-packages", {})) else: for category in categories: if category == "default" or category == "packages": - data["default"] = _copy_jsonsafe(pipfile._data.get("packages", {})) + data["default"] = _copy_jsonsafe(getattr(pipfile,"packages", {})) elif category == "develop" or category == "dev-packages": - data["develop"] = _copy_jsonsafe(pipfile._data.get("dev-packages", {})) + data["develop"] = _copy_jsonsafe(getattr(pipfile,"dev-packages", {})) else: - data[category] = _copy_jsonsafe(pipfile._data.get(category, {})) + data[category] = _copy_jsonsafe(getattr(pipfile, category, {})) if "default" not in data: data["default"] = {} if "develop" not in data: @@ -149,13 +129,7 @@ def is_up_to_date(self, pipfile): return self.meta.hash == pipfile.get_hash() def dump(self, fh, encoding=None): - encoder = _LockFileEncoder() - if encoding is None: - for chunk in encoder.iterencode(self): - fh.write(chunk) - else: - content = encoder.encode(self) - fh.write(content.encode(encoding)) + json.dump(self, fh, cls=DCJSONEncoder) self.meta = self._meta @property diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 8db4f96..7baddac 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -3,8 +3,11 @@ import tomlkit +from dataclasses import dataclass, field + +from typing import Optional from .models import ( - DataView, Hash, Requires, PipfileSection, Pipenv, + Hash, Requires, PipfileSection, Pipenv, PackageCollection, ScriptCollection, SourceCollection, ) @@ -26,27 +29,39 @@ verify_ssl = true """ -class Pipfile(DataView): - """Representation of a Pipfile. - """ - __SCHEMA__ = {} +@dataclass +class Pipfile: + """Representation of a Pipfile.""" + sources: SourceCollection + packages: Optional[PackageCollection] = None - @classmethod - def validate(cls, data): - # HACK: DO NOT CALL `super().validate()` here!! - # Cerberus seems to break TOML Kit's inline table preservation if it - # is not at the top-level. Fortunately the spec doesn't have nested - # non-inlined tables, so we're OK as long as validation is only - # performed at section-level. validation is performed. - for key, klass in PIPFILE_SECTIONS.items(): - if key not in data: + def get_hash(self): + data = { + "_meta": { + "sources": getattr(self, "source", {}), + "requires": getattr(self, "requires", {}), + }, + "default": getattr(self, "packages", {}), + "develop": getattr(self, "dev-packages", {}), + } + for category, values in self.__dict__.items(): + if category in PIPFILE_SECTIONS or category in ("default", "develop", "pipenv"): continue - klass.validate(data[key]) + data[category] = values + content = json.dumps(data, sort_keys=True, separators=(",", ":")) + if isinstance(content, str): + content = content.encode("utf-8") + return Hash.from_hash(hashlib.sha256(content)) - package_categories = set(data.keys()) - set(PIPFILE_SECTIONS.keys()) - for category in package_categories: - PackageCollection.validate(data[category]) +class Foo: + source: SourceCollection + packages: Optional[PackageCollection] = None + dev_packages: PackageCollection + requires: Requires + scripts: ScriptCollection + pipfile: PipfileSection + pipenv: Pipenv @classmethod def load(cls, f, encoding=None): @@ -65,28 +80,22 @@ def load(cls, f, encoding=None): return cls(data) def __getitem__(self, key): - value = self._data[key] + value = self[key] try: return PIPFILE_SECTIONS[key](value) except KeyError: return value - def __setitem__(self, key, value): - if isinstance(value, DataView): - self._data[key] = value._data - else: - self._data[key] = value - def get_hash(self): data = { "_meta": { - "sources": self._data["source"], - "requires": self._data.get("requires", {}), + "sources": self["source"], + "requires": getattr(self, "requires", {}), }, - "default": self._data.get("packages", {}), - "develop": self._data.get("dev-packages", {}), + "default": getattr(self, "packages", {}), + "develop": getattr(self, "dev-packages", {}), } - for category, values in self._data.items(): + for category, values in self.__dict__.items(): if category in PIPFILE_SECTIONS or category in ("default", "develop", "pipenv"): continue data[category] = values From f675b1c5d3d1708732649e078967cf51014a1cca Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 19 Nov 2023 21:53:09 +0100 Subject: [PATCH 34/59] Remove break point tasks/__init__.py --- tasks/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tasks/__init__.py b/tasks/__init__.py index 64c3026..a392974 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -53,7 +53,6 @@ def bump_release(ctx, type_): raise ValueError(f'{type_} not in {REL_TYPES}') index = REL_TYPES.index(type_) prev_version = _read_version() - import pdb; pdb.set_trace() if prev_version.is_prerelease: next_version = prev_version.base_version() else: From 0c03c776f854fb030b8ef60218808be07ec39e88 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 19 Nov 2023 21:53:49 +0100 Subject: [PATCH 35/59] Fix one more tests for lockfiles --- src/plette/lockfiles.py | 53 +++++++++++++++++++++++++++++------ src/plette/models/packages.py | 14 +++++++-- tests/test_lockfiles.py | 6 ++-- tests/test_models_packages.py | 6 ++-- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index a6b1608..9f8c924 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -2,6 +2,7 @@ # pylint: disable=missing-function-docstring # pylint: disable=no-member +import dataclasses import json import numbers @@ -12,13 +13,52 @@ from .models import Meta, PackageCollection, Package - -import dataclasses, json +def flatten_versions(d): + copy = {} + # Iterate over a copy of the dictionary + for key, value in d.items(): + # If the value is a dictionary, call the function recursively + #if isinstance(value, dict): + # flatten_dict(value) + # If the key is "version", replace the key with the value + copy[key] = value["version"] + return copy + +def remove_empty_values(d): + # Iterate over a copy of the dictionary + for key, value in list(d.items()): + # If the value is a dictionary, call the function recursively + if isinstance(value, dict): + remove_empty_values(value) + # If the dictionary is empty, remove the key + if not value: + del d[key] + # If the value is None or an empty string, remove the key + elif value is None or value == '': + del d[key] class DCJSONEncoder(json.JSONEncoder): def default(self, o): if dataclasses.is_dataclass(o): - return dataclasses.asdict(o) + o = dataclasses.asdict(o) + if "_meta" in o: + o["_meta"]["pipfile-spec"] = o["_meta"].pop("pipfile_spec") + o["_meta"]["hash"] = {o["_meta"]["hash"]["name"]: o["_meta"]["hash"]["value"]} + o["_meta"]["sources"] = o["_meta"]["sources"].pop("sources") + + remove_empty_values(o) + + for section in ["default", "develop"]: + try: + o[section] = flatten_versions(o[section]) + except KeyError: + continue + # add silly default values + if "develop" not in o: + o["develop"] = {} + if "requires" not in o["_meta"]: + o["_meta"]["requires"] = {} + return o return super().default(o) PIPFILE_SPEC_CURRENT = 6 @@ -27,6 +67,7 @@ def default(self, o): def _copy_jsonsafe(value): """Deep-copy a value into JSON-safe types. """ + import pdb; pdb.set_trace() if isinstance(value, (str, numbers.Number)): return value if isinstance(value, collections_abc.Mapping): @@ -73,11 +114,7 @@ def validate_meta(self, value, field): def validate_default(self, value, field): packages = {} for name, spec in value.items(): - if isinstance(spec, str): - packages[name] = Package(spec) - else: - packages[name] = Package(**spec) - + packages[name] = Package(spec) return packages @classmethod diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index 656728f..1009c93 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -2,7 +2,7 @@ # pylint: disable=missing-function-docstring # pylint: disable=no-member from dataclasses import dataclass -from typing import Optional, List +from typing import Optional, List, Union @dataclass @@ -25,7 +25,7 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) - version: Optional[str] = None + version: Union[Optional[str],Optional[dict]] = None specifiers: Optional[PackageSpecfiers] = None editable: Optional[bool] = None extras: Optional[PackageSpecfiers] = None @@ -37,3 +37,13 @@ def validate_extras(self, value, field): if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): raise ValueError("Extras must be a list or None") return value + + def validate_version(self, value, field): + if isinstance(value, dict): + return value + if isinstance(value, str): + return value + if value is None: + return None + + raise ValueError(f"Unknown type {type(value)} for version") diff --git a/tests/test_lockfiles.py b/tests/test_lockfiles.py index dcf3c15..9a61c5b 100644 --- a/tests/test_lockfiles.py +++ b/tests/test_lockfiles.py @@ -1,6 +1,7 @@ # pylint: disable=missing-module-docstring,missing-class-docstring # pylint: disable=missing-function-docstring # pylint: disable=no-member +import json import textwrap from plette import Lockfile, Pipfile @@ -111,8 +112,9 @@ def test_lockfile_dump_format(tmpdir): outpath = tmpdir.join("out.json") with outpath.open("w") as f: lock.dump(f) - - assert outpath.read() == content + loaded = json.loads(outpath.read()) + assert "_meta" in loaded + assert json.loads(outpath.read()) == json.loads(content) def test_lockfile_from_pipfile_meta(): diff --git a/tests/test_models_packages.py b/tests/test_models_packages.py index 8a81446..86d04ab 100644 --- a/tests/test_models_packages.py +++ b/tests/test_models_packages.py @@ -4,12 +4,12 @@ def test_package_str(): p = Package("*") - p.version == "*" + assert p.version == "*" def test_package_dict(): - p = Package(**{"version": "*"}) - p.version == "*" + p = Package({"version": "*"}) + assert p.version == {"version": "*"} def test_package_version_is_none(): From 6d6bbfe7c9064a0c3a6e984858e042bd58260258 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 19 Nov 2023 21:54:58 +0100 Subject: [PATCH 36/59] Remove breakpoint from lockfiles.py --- src/plette/lockfiles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index 9f8c924..8931f8d 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -67,7 +67,6 @@ def default(self, o): def _copy_jsonsafe(value): """Deep-copy a value into JSON-safe types. """ - import pdb; pdb.set_trace() if isinstance(value, (str, numbers.Number)): return value if isinstance(value, collections_abc.Mapping): From 13028a351d32670ed2ad00037eb82eebc65528e5 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 22 Nov 2023 23:43:20 +0100 Subject: [PATCH 37/59] All tests for lockfiles pass --- src/plette/lockfiles.py | 2 +- src/plette/pipfiles.py | 16 +++++++++++----- tests/test_lockfiles.py | 17 +++++++++-------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index 8931f8d..fb09e31 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -128,7 +128,7 @@ def load(cls, fh, encoding=None): def with_meta_from(cls, pipfile, categories=None): data = { "_meta": { - "hash": _copy_jsonsafe(pipfile.get_hash()), + "hash": pipfile.get_hash().__dict__, "pipfile-spec": PIPFILE_SPEC_CURRENT, "requires": _copy_jsonsafe(getattr(pipfile, "requires", {})), "sources": _copy_jsonsafe(pipfile.sources), diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 7baddac..53fd052 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -34,6 +34,12 @@ class Pipfile: """Representation of a Pipfile.""" sources: SourceCollection packages: Optional[PackageCollection] = None + packages: Optional[PackageCollection] = None + dev_packages: Optional[PackageCollection] = None + requires: Optional[Requires] = None + scripts: Optional[ScriptCollection] = None + pipfile: Optional[PipfileSection] = None + pipenv: Optional[Pipenv] = None def get_hash(self): data = { @@ -57,11 +63,11 @@ def get_hash(self): class Foo: source: SourceCollection packages: Optional[PackageCollection] = None - dev_packages: PackageCollection - requires: Requires - scripts: ScriptCollection - pipfile: PipfileSection - pipenv: Pipenv + dev_packages: Optional[PackageCollection] = None + requires: Optional[Requires] = None + scripts: Optional[ScriptCollection] = None + pipfile: Optional[PipfileSection] = None + pipenv: Optional[Pipenv] = None @classmethod def load(cls, f, encoding=None): diff --git a/tests/test_lockfiles.py b/tests/test_lockfiles.py index 9a61c5b..b3d1162 100644 --- a/tests/test_lockfiles.py +++ b/tests/test_lockfiles.py @@ -5,7 +5,7 @@ import textwrap from plette import Lockfile, Pipfile -from plette.models import Package, SourceCollection +from plette.models import Package, SourceCollection, Hash, Requires HASH = "9aaf3dbaf8c4df3accd4606eb2275d3b91c9db41be4fd5a97ecc95d79a12cfe6" @@ -118,8 +118,8 @@ def test_lockfile_dump_format(tmpdir): def test_lockfile_from_pipfile_meta(): - pipfile = Pipfile({ - "source": [ + pipfile = Pipfile(**{ + "sources": [ { "name": "pypi", "url": "https://pypi.org/simple", @@ -130,22 +130,23 @@ def test_lockfile_from_pipfile_meta(): "python_version": "3.7", } }) + pipfile_hash_value = pipfile.get_hash().value lockfile = Lockfile.with_meta_from(pipfile) - pipfile.requires._data["python_version"] = "3.8" + pipfile.requires["python_version"] = "3.8" pipfile.sources.append({ "name": "devpi", "url": "http://localhost/simple", "verify_ssl": True, }) - assert lockfile.meta.hash._data == {"sha256": pipfile_hash_value} - assert lockfile.meta.requires._data == {"python_version": "3.7"} - assert lockfile.meta.sources._data == [ + assert lockfile.meta.hash == Hash.from_dict({"sha256": pipfile_hash_value}) + assert lockfile.meta.requires == Requires(python_version={'python_version': '3.7'}, python_full_version=None) + assert lockfile.meta.sources == SourceCollection([ { "name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": True, }, - ] + ]) From 63a16c2103910ab4ef14284e23b5ccbccbb10a7b Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 23 Nov 2023 00:55:03 +0100 Subject: [PATCH 38/59] 2 more tests for pipfiles pass now --- src/plette/models/packages.py | 4 ++-- src/plette/models/sections.py | 16 +++++++++++++++- src/plette/pipfiles.py | 20 ++++++++++++++++++++ tests/test_pipfiles.py | 15 ++++++--------- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index 1009c93..0123c19 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -25,7 +25,7 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) - version: Union[Optional[str],Optional[dict]] = None + version: Union[Optional[str],Optional[dict]] = "*" specifiers: Optional[PackageSpecfiers] = None editable: Optional[bool] = None extras: Optional[PackageSpecfiers] = None @@ -44,6 +44,6 @@ def validate_version(self, value, field): if isinstance(value, str): return value if value is None: - return None + return "*" raise ValueError(f"Unknown type {type(value)} for version") diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index 895a1fc..99269e9 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -15,12 +15,26 @@ class PackageCollection: packages: List[Package] + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_packages(self, value, field): + packages = {k:Package(v) for k, v in value.items()} + return packages @dataclass class ScriptCollection: scripts: List[Script] - @dataclass class SourceCollection: diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 53fd052..d3ad21b 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -59,6 +59,26 @@ def get_hash(self): content = content.encode("utf-8") return Hash.from_hash(hashlib.sha256(content)) + @classmethod + def load(cls, f, encoding=None): + content = f.read() + if encoding is not None: + content = content.decode(encoding) + data = tomlkit.loads(content) + if "source" not in data: + # HACK: There is no good way to prepend a section to an existing + # TOML document, but there's no good way to copy non-structural + # content from one TOML document to another either. Modify the + # TOML content directly, and load the new in-memory document. + sep = "" if content.startswith("\n") else "\n" + content = DEFAULT_SOURCE_TOML + sep + content + data = tomlkit.loads(content) + return cls(data) + + @property + def source(self): + return self.sources[0] + class Foo: source: SourceCollection diff --git a/tests/test_pipfiles.py b/tests/test_pipfiles.py index 29e0493..705050f 100644 --- a/tests/test_pipfiles.py +++ b/tests/test_pipfiles.py @@ -27,13 +27,13 @@ def test_source_section_transparent(): }, ]) section[0].verify_ssl = True - assert section._data == [ + assert section == SourceCollection([ { "name": "devpi", "url": "https://$USER:$PASS@mydevpi.localhost", "verify_ssl": True, }, - ] + ]) def test_package_section(): @@ -41,10 +41,7 @@ def test_package_section(): "flask": {"version": "*"}, "jinja2": "*", }) - assert section["jinja2"].version == "*" - with pytest.raises(KeyError) as ctx: - section["mosql"] - assert str(ctx.value) == repr("mosql") + assert section.packages["jinja2"].version == "*" def test_pipfile_load(tmpdir): @@ -55,14 +52,14 @@ def test_pipfile_load(tmpdir): jinja2 = '*' # A comment. """)) p = Pipfile.load(fi) - assert p["source"] == SourceCollection([ + assert p.source == SourceCollection([ { 'url': 'https://pypi.org/simple', 'verify_ssl': True, 'name': 'pypi', }, ]) - assert p["packages"] == PackageCollection({ + assert p["packages"] == PackageCollection(**{ "flask": {"version": "*"}, "jinja2": "*", }) @@ -78,7 +75,7 @@ def test_pipfile_preserve_format(tmpdir): """, )) p = Pipfile.load(fi) - p["source"][0].verify_ssl = False + p.source.verify_ssl = False fo = tmpdir.join("Pipfile.out") p.dump(fo) From 172cd1e7c5fe91c86524481697818d6ec3d5edd7 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 23 Nov 2023 01:30:56 +0100 Subject: [PATCH 39/59] All pipefile tests work except for dump --- src/plette/pipfiles.py | 140 +++++++---------------------------------- tests/test_pipfiles.py | 7 +-- 2 files changed, 24 insertions(+), 123 deletions(-) diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index d3ad21b..85f6517 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -41,6 +41,22 @@ class Pipfile: pipfile: Optional[PipfileSection] = None pipenv: Optional[Pipenv] = None + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_sources(self, value, field): + return SourceCollection(value.value) + def get_hash(self): data = { "_meta": { @@ -73,131 +89,17 @@ def load(cls, f, encoding=None): sep = "" if content.startswith("\n") else "\n" content = DEFAULT_SOURCE_TOML + sep + content data = tomlkit.loads(content) - return cls(data) + data["sources"] = data.pop("source") + return cls(**data) @property def source(self): - return self.sources[0] - - -class Foo: - source: SourceCollection - packages: Optional[PackageCollection] = None - dev_packages: Optional[PackageCollection] = None - requires: Optional[Requires] = None - scripts: Optional[ScriptCollection] = None - pipfile: Optional[PipfileSection] = None - pipenv: Optional[Pipenv] = None - - @classmethod - def load(cls, f, encoding=None): - content = f.read() - if encoding is not None: - content = content.decode(encoding) - data = tomlkit.loads(content) - if "source" not in data: - # HACK: There is no good way to prepend a section to an existing - # TOML document, but there's no good way to copy non-structural - # content from one TOML document to another either. Modify the - # TOML content directly, and load the new in-memory document. - sep = "" if content.startswith("\n") else "\n" - content = DEFAULT_SOURCE_TOML + sep + content - data = tomlkit.loads(content) - return cls(data) - - def __getitem__(self, key): - value = self[key] - try: - return PIPFILE_SECTIONS[key](value) - except KeyError: - return value - - def get_hash(self): - data = { - "_meta": { - "sources": self["source"], - "requires": getattr(self, "requires", {}), - }, - "default": getattr(self, "packages", {}), - "develop": getattr(self, "dev-packages", {}), - } - for category, values in self.__dict__.items(): - if category in PIPFILE_SECTIONS or category in ("default", "develop", "pipenv"): - continue - data[category] = values - content = json.dumps(data, sort_keys=True, separators=(",", ":")) - if isinstance(content, str): - content = content.encode("utf-8") - return Hash.from_hash(hashlib.sha256(content)) + return self.sources def dump(self, f, encoding=None): - content = tomlkit.dumps(self._data) + content = tomlkit.dumps(self) if encoding is not None: content = content.encode(encoding) f.write(content) - @property - def sources(self): - try: - return self["source"] - except KeyError: - raise AttributeError("sources") - - @sources.setter - def sources(self, value): - self["source"] = value - - @property - def source(self): - try: - return self["source"] - except KeyError: - raise AttributeError("source") - - @source.setter - def source(self, value): - self["source"] = value - - @property - def packages(self): - try: - return self["packages"] - except KeyError: - raise AttributeError("packages") - - @packages.setter - def packages(self, value): - self["packages"] = value - - @property - def dev_packages(self): - try: - return self["dev-packages"] - except KeyError: - raise AttributeError("dev-packages") - - @dev_packages.setter - def dev_packages(self, value): - self["dev-packages"] = value - - @property - def requires(self): - try: - return self["requires"] - except KeyError: - raise AttributeError("requires") - - @requires.setter - def requires(self, value): - self["requires"] = value - - @property - def scripts(self): - try: - return self["scripts"] - except KeyError: - raise AttributeError("scripts") - - @scripts.setter - def scripts(self, value): - self["scripts"] = value + # todo add a method make pipfile behave like a dict so dump works diff --git a/tests/test_pipfiles.py b/tests/test_pipfiles.py index 705050f..4bceb09 100644 --- a/tests/test_pipfiles.py +++ b/tests/test_pipfiles.py @@ -1,7 +1,5 @@ import textwrap -import pytest - from plette import Pipfile from plette.models import PackageCollection, SourceCollection @@ -52,6 +50,7 @@ def test_pipfile_load(tmpdir): jinja2 = '*' # A comment. """)) p = Pipfile.load(fi) + assert p.source == SourceCollection([ { 'url': 'https://pypi.org/simple', @@ -59,10 +58,10 @@ def test_pipfile_load(tmpdir): 'name': 'pypi', }, ]) - assert p["packages"] == PackageCollection(**{ + assert p.packages == { "flask": {"version": "*"}, "jinja2": "*", - }) + } def test_pipfile_preserve_format(tmpdir): From 0c71b9da973eb85a6726f20199166c0f4c18260a Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 23 Nov 2023 02:38:39 +0100 Subject: [PATCH 40/59] All tests pass --- src/plette/lockfiles.py | 22 ++-- src/plette/models/__init__.py | 5 - src/plette/models/base.py | 191 ---------------------------------- src/plette/models/sources.py | 1 - src/plette/pipfiles.py | 36 ++++++- tests/test_lockfiles.py | 2 +- tests/test_models_packages.py | 4 +- tests/test_models_sources.py | 2 + tests/test_pipfiles.py | 8 +- 9 files changed, 51 insertions(+), 220 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index fb09e31..cc66be3 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -8,21 +8,21 @@ import collections.abc as collections_abc -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict from typing import Optional from .models import Meta, PackageCollection, Package def flatten_versions(d): - copy = {} - # Iterate over a copy of the dictionary - for key, value in d.items(): - # If the value is a dictionary, call the function recursively - #if isinstance(value, dict): - # flatten_dict(value) - # If the key is "version", replace the key with the value - copy[key] = value["version"] - return copy + copy = {} + # Iterate over a copy of the dictionary + for key, value in d.items(): + # If the value is a dictionary, call the function recursively + #if isinstance(value, dict): + # flatten_dict(value) + # If the key is "version", replace the key with the value + copy[key] = value["version"] + return copy def remove_empty_values(d): # Iterate over a copy of the dictionary @@ -131,9 +131,9 @@ def with_meta_from(cls, pipfile, categories=None): "hash": pipfile.get_hash().__dict__, "pipfile-spec": PIPFILE_SPEC_CURRENT, "requires": _copy_jsonsafe(getattr(pipfile, "requires", {})), - "sources": _copy_jsonsafe(pipfile.sources), }, } + data["_meta"].update(asdict(pipfile.sources)) if categories is None: data["default"] = _copy_jsonsafe(getattr(pipfile, "packages", {})) data["develop"] = _copy_jsonsafe(getattr(pipfile, "dev-packages", {})) diff --git a/src/plette/models/__init__.py b/src/plette/models/__init__.py index 93fd4a5..8bdb49b 100644 --- a/src/plette/models/__init__.py +++ b/src/plette/models/__init__.py @@ -5,11 +5,6 @@ "Meta", "PackageCollection", "ScriptCollection", "SourceCollection", ] -from .base import ( - DataView, NewDataView, DataViewCollection, DataViewMapping, DataViewSequence, - validate, ValidationError, -) - from .hashes import Hash from .packages import Package from .scripts import Script diff --git a/src/plette/models/base.py b/src/plette/models/base.py index e71fd12..eb04985 100644 --- a/src/plette/models/base.py +++ b/src/plette/models/base.py @@ -1,195 +1,4 @@ from dataclasses import dataclass -try: - import cerberus -except ImportError: - cerberus = None - class ValidationError(ValueError): pass - -VALIDATORS = {} - - -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 NewModel: - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - -@dataclass -class NewDataView(NewModel): - - _data: dict - - def __repr__(self): - return "{0}({1!r})".format(type(self).__name__, self._data) - - def __eq__(self, other): - if not isinstance(other, type(self)): - raise TypeError( - "cannot compare {0!r} with {1!r}".format( - type(self).__name__, type(other).__name__ - ) - ) - return self._data == other._data - - def __getitem__(self, key): - return self._data[key] - - def __setitem__(self, key, value): - self._data[key] = value - - def __delitem__(self, key): - del self._data[key] - - def get(self, key, default=None): - try: - return self[key] - except KeyError: - return default - - -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. - """ - - def __init__(self, data): - self.validate(data) - self._data = data - - def __repr__(self): - return "{0}({1!r})".format(type(self).__name__, self._data) - - def __eq__(self, other): - if not isinstance(other, type(self)): - raise TypeError( - "cannot compare {0!r} with {1!r}".format( - type(self).__name__, type(other).__name__ - ) - ) - return self._data == other._data - - def __getitem__(self, key): - return self._data[key] - - def __setitem__(self, key, value): - self._data[key] = value - - def __delitem__(self, key): - del self._data[key] - - def get(self, key, default=None): - try: - return self[key] - except KeyError: - return default - - @classmethod - def validate(cls, data): - return validate(cls, data) - - -class DataViewCollection(DataView): - """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` - protocol. - - You should not instantiate an instance from this class, but from one of its - subclasses instead. - """ - - item_class = None - - def __repr__(self): - return "{0}({1!r})".format(type(self).__name__, self._data) - - def __len__(self): - return len(self._data) - - def __getitem__(self, key): - return self.item_class(self._data[key]) - - def __setitem__(self, key, value): - if isinstance(value, DataView): - value = value._data - self._data[key] = value - - def __delitem__(self, key): - del self._data[key] - - -class DataViewMapping(DataViewCollection): - """A mapping of data views. - - The keys are primitive values, while values are instances of `item_class`. - """ - - @classmethod - def validate(cls, data): - for d in data.values(): - cls.item_class.validate(d) - - def __iter__(self): - return iter(self._data) - - def keys(self): - return self._data.keys() - - def values(self): - return [self[k] for k in self._data] - - def items(self): - return [(k, self[k]) for k in self._data] - - -class DataViewSequence(DataViewCollection): - """A sequence of data views. - - Each entry is an instance of `item_class`. - """ - - @classmethod - def validate(cls, data): - for d in data: - cls.item_class.validate(d) - - def __iter__(self): - return (self.item_class(d) for d in self._data) - - def __getitem__(self, key): - if isinstance(key, slice): - return type(self)(self._data[key]) - return super(DataViewSequence, self).__getitem__(key) - - def append(self, value): - if isinstance(value, DataView): - value = value._data - self._data.append(value) diff --git a/src/plette/models/sources.py b/src/plette/models/sources.py index c6fa45d..e41f61a 100644 --- a/src/plette/models/sources.py +++ b/src/plette/models/sources.py @@ -20,7 +20,6 @@ class Source: """ name: str verify_ssl: bool - #url: Optional[str] = None url: str @property diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 85f6517..e04e6ec 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -3,7 +3,7 @@ import tomlkit -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict from typing import Optional from .models import ( @@ -11,9 +11,21 @@ PackageCollection, ScriptCollection, SourceCollection, ) +def remove_empty_values(d): + # Iterate over a copy of the dictionary + for key, value in list(d.items()): + # If the value is a dictionary, call the function recursively + if isinstance(value, dict): + remove_empty_values(value) + # If the dictionary is empty, remove the key + if not value: + del d[key] + # If the value is None or an empty string, remove the key + elif value is None or value == '': + del d[key] PIPFILE_SECTIONS = { - "source": SourceCollection, + "sources": SourceCollection, "packages": PackageCollection, "dev-packages": PackageCollection, "requires": Requires, @@ -55,21 +67,28 @@ def __post_init__(self): setattr(self, name, method(getattr(self, name), field=field)) def validate_sources(self, value, field): + if isinstance(value, list): + return SourceCollection(value) return SourceCollection(value.value) - def get_hash(self): + def to_dict(self): data = { "_meta": { - "sources": getattr(self, "source", {}), "requires": getattr(self, "requires", {}), }, "default": getattr(self, "packages", {}), "develop": getattr(self, "dev-packages", {}), } + data["_meta"].update(asdict(getattr(self, "sources", {}))) for category, values in self.__dict__.items(): if category in PIPFILE_SECTIONS or category in ("default", "develop", "pipenv"): continue data[category] = values + remove_empty_values(data) + return data + + def get_hash(self): + data = self.to_dict() content = json.dumps(data, sort_keys=True, separators=(",", ":")) if isinstance(content, str): content = content.encode("utf-8") @@ -97,7 +116,14 @@ def source(self): return self.sources def dump(self, f, encoding=None): - content = tomlkit.dumps(self) + data = self.to_dict() + new_data = {} + metadata = data.pop("_meta") + new_data["source"] = metadata.pop("sources") + new_data["packages"] = data.pop("default") + new_data.update(data) + content = tomlkit.dumps(new_data) + if encoding is not None: content = content.encode(encoding) f.write(content) diff --git a/tests/test_lockfiles.py b/tests/test_lockfiles.py index b3d1162..6d03bf2 100644 --- a/tests/test_lockfiles.py +++ b/tests/test_lockfiles.py @@ -135,7 +135,7 @@ def test_lockfile_from_pipfile_meta(): lockfile = Lockfile.with_meta_from(pipfile) pipfile.requires["python_version"] = "3.8" - pipfile.sources.append({ + pipfile.sources.sources.append({ "name": "devpi", "url": "http://localhost/simple", "verify_ssl": True, diff --git a/tests/test_models_packages.py b/tests/test_models_packages.py index 86d04ab..91b973d 100644 --- a/tests/test_models_packages.py +++ b/tests/test_models_packages.py @@ -14,7 +14,7 @@ def test_package_dict(): def test_package_version_is_none(): p = Package(**{"path": ".", "editable": True}) - assert p.version == None + assert p.version == "*" assert p.editable is True def test_package_with_wrong_extras(): @@ -36,4 +36,4 @@ def test_package_with_extras(): def test_package_wrong_key(): p = Package(**{"path": ".", "editable": True}) assert p.editable is True - assert p.version is None + assert p.version is "*" diff --git a/tests/test_models_sources.py b/tests/test_models_sources.py index 0a03f2a..9892290 100644 --- a/tests/test_models_sources.py +++ b/tests/test_models_sources.py @@ -1,4 +1,6 @@ +import pytest from plette.models.sources import Source +from plette import models def test_source_from_data(): s = Source( diff --git a/tests/test_pipfiles.py b/tests/test_pipfiles.py index 4bceb09..203b2e5 100644 --- a/tests/test_pipfiles.py +++ b/tests/test_pipfiles.py @@ -73,17 +73,17 @@ def test_pipfile_preserve_format(tmpdir): jinja2 = '*' """, )) - p = Pipfile.load(fi) - p.source.verify_ssl = False + pf= Pipfile.load(fi) + pf.source[0].verify_ssl = False fo = tmpdir.join("Pipfile.out") - p.dump(fo) + pf.dump(fo) assert fo.read() == textwrap.dedent( """\ [[source]] name = "pypi" - url = "https://pypi.org/simple" verify_ssl = false + url = "https://pypi.org/simple" [packages] flask = { version = "*" } From da05a0d22d7d93ddb9f6df5ff9deeb5f52e4f069 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 23 Nov 2023 02:49:38 +0100 Subject: [PATCH 41/59] Fix validation test for allow_prereleases --- src/plette/models/sections.py | 21 ++++++++++++++++++++- src/plette/pipfiles.py | 3 +++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index 99269e9..7b96586 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import Optional, List +from .base import ValidationError from .hashes import Hash from .packages import Package from .scripts import Script @@ -171,5 +172,23 @@ def validate_pipfile_spec(self, value, field): @dataclass class Pipenv: """Represent the [pipenv] section in Pipfile""" - allow_prereleases: Optional[bool] + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_allow_prereleases(self, value, field): + import pdb; pdb.set_trace() + if not isinstance(value, bool): + raise ValidationError('allow_prereleases must be a boolean') + + diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index e04e6ec..3a68c79 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -71,6 +71,9 @@ def validate_sources(self, value, field): return SourceCollection(value) return SourceCollection(value.value) + def validate_pipenv(self, value, field): + return Pipenv(**value) + def to_dict(self): data = { "_meta": { From 093019e4e2cf91ceebbd759398e14f7fb5faaf2e Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Fri, 24 Nov 2023 18:38:16 +0100 Subject: [PATCH 42/59] Fix all tests for lockfiles --- src/plette/models/sections.py | 1 - src/plette/pipfiles.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index 7b96586..18c6c08 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -187,7 +187,6 @@ def __post_init__(self): setattr(self, name, method(getattr(self, name), field=field)) def validate_allow_prereleases(self, value, field): - import pdb; pdb.set_trace() if not isinstance(value, bool): raise ValidationError('allow_prereleases must be a boolean') diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 3a68c79..cfcc15c 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -72,7 +72,8 @@ def validate_sources(self, value, field): return SourceCollection(value.value) def validate_pipenv(self, value, field): - return Pipenv(**value) + if value is not None: + return Pipenv(**value) def to_dict(self): data = { From c7be25498b467ab955156eb21884692a9a7c4dc4 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Fri, 24 Nov 2023 23:55:25 +0100 Subject: [PATCH 43/59] All tests for pipfile now pass 3 failing integration tests ... --- src/plette/models/sections.py | 8 +++++++- src/plette/pipfiles.py | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index 18c6c08..f2dfc0f 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -172,7 +172,8 @@ def validate_pipfile_spec(self, value, field): @dataclass class Pipenv: """Represent the [pipenv] section in Pipfile""" - allow_prereleases: Optional[bool] + allow_prereleases: Optional[bool] = False + install_search_all_sources: Optional[bool] = True def __post_init__(self): """Run validation methods if declared. @@ -189,5 +190,10 @@ def __post_init__(self): def validate_allow_prereleases(self, value, field): if not isinstance(value, bool): raise ValidationError('allow_prereleases must be a boolean') + return value + def validate_install_search_all_sources(self, value, field): + if not isinstance(value, bool): + raise ValidationError('install_search_all_sources must be a boolean') + return value diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index cfcc15c..8b2db11 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -75,6 +75,9 @@ def validate_pipenv(self, value, field): if value is not None: return Pipenv(**value) + def validate_packages(self, value, field): + return value + def to_dict(self): data = { "_meta": { @@ -113,7 +116,17 @@ def load(cls, f, encoding=None): content = DEFAULT_SOURCE_TOML + sep + content data = tomlkit.loads(content) data["sources"] = data.pop("source") - return cls(**data) + packages_sections = {} + data_sections = list(data.keys()) + for k in data_sections: + if k not in cls.__dataclass_fields__: + packages_sections[k] = data.pop(k) + + inst = cls(**data) + if packages_sections: + for k, v in packages_sections.items(): + setattr(inst, k, v) + return inst @property def source(self): From 246420bc94834391555783d5a74933e2c0244558 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 25 Nov 2023 23:31:38 +0100 Subject: [PATCH 44/59] All tests pass! --- examples/Pipfile.invalid.extras-list | 2 +- examples/Pipfile.invalid.with-categories | 2 ++ src/plette/models/packages.py | 29 ++++++++++++++++++++++-- src/plette/models/sections.py | 11 +++++++-- src/plette/pipfiles.py | 3 ++- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/examples/Pipfile.invalid.extras-list b/examples/Pipfile.invalid.extras-list index 05d8e17..321042f 100644 --- a/examples/Pipfile.invalid.extras-list +++ b/examples/Pipfile.invalid.extras-list @@ -1,4 +1,4 @@ -# package specifiers should be a dict +# package extras should be a list [packages] msal = {version= "==1.20.0", extras = "broker"} diff --git a/examples/Pipfile.invalid.with-categories b/examples/Pipfile.invalid.with-categories index c9d7b01..39bb547 100644 --- a/examples/Pipfile.invalid.with-categories +++ b/examples/Pipfile.invalid.with-categories @@ -1,3 +1,5 @@ +# this file contains a section with one package with wrong extras +# extras should be passed as a list [packages] plette = { path = '.', extras = ['validation'], editable = true } diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py index 0123c19..6ed463e 100644 --- a/src/plette/models/packages.py +++ b/src/plette/models/packages.py @@ -1,14 +1,32 @@ # pylint: disable=missing-module-docstring,missing-class-docstring # pylint: disable=missing-function-docstring # pylint: disable=no-member + from dataclasses import dataclass from typing import Optional, List, Union +from .base import ValidationError @dataclass class PackageSpecfiers: extras: List[str] + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_extras(self, value, field): + if not isinstance(value, list): + raise ValidationError("Extras must be a list") + @dataclass class Package: @@ -35,7 +53,7 @@ def validate_extras(self, value, field): if value is None: return value if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): - raise ValueError("Extras must be a list or None") + raise ValidationError("Extras must be a list or None") return value def validate_version(self, value, field): @@ -46,4 +64,11 @@ def validate_version(self, value, field): if value is None: return "*" - raise ValueError(f"Unknown type {type(value)} for version") + raise ValidationError(f"Unknown type {type(value)} for version") + + def validate_extras(self, value, field): + if value is None: + return value + if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): + raise ValidationError("Extras must be a list or None") + return value diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py index f2dfc0f..91c3116 100644 --- a/src/plette/models/sections.py +++ b/src/plette/models/sections.py @@ -29,8 +29,15 @@ def __post_init__(self): setattr(self, name, method(getattr(self, name), field=field)) def validate_packages(self, value, field): - packages = {k:Package(v) for k, v in value.items()} - return packages + if isinstance(value, dict): + packages = {} + for k, v in value.items(): + if isinstance(v, dict): + packages[k] = Package(**v) + else: + packages[k] = Package(version=v) + return packages + @dataclass class ScriptCollection: diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 8b2db11..2aa5735 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -76,6 +76,7 @@ def validate_pipenv(self, value, field): return Pipenv(**value) def validate_packages(self, value, field): + PackageCollection(value) return value def to_dict(self): @@ -125,7 +126,7 @@ def load(cls, f, encoding=None): inst = cls(**data) if packages_sections: for k, v in packages_sections.items(): - setattr(inst, k, v) + setattr(inst, k, PackageCollection(v)) return inst @property From f8690db4bf349797e3c51af39eeaf9687be4593b Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Tue, 28 Nov 2023 23:53:40 +0100 Subject: [PATCH 45/59] Merge all models into one file --- src/plette/models.py | 355 +++++++++++++++++++++++++++++ src/plette/models/__init__.py | 21 -- src/plette/models/base.py | 4 - src/plette/models/hashes.py | 66 ------ src/plette/models/packages.py | 74 ------ src/plette/models/scripts.py | 77 ------- src/plette/models/sections.py | 206 ----------------- src/plette/models/sources.py | 44 ---- src/plette/pipfiles.py | 6 +- tests/integration/test_examples.py | 2 +- tests/test_models_hash.py | 2 +- tests/test_models_sources.py | 4 +- 12 files changed, 363 insertions(+), 498 deletions(-) create mode 100644 src/plette/models.py delete mode 100644 src/plette/models/__init__.py delete mode 100644 src/plette/models/base.py delete mode 100644 src/plette/models/hashes.py delete mode 100644 src/plette/models/packages.py delete mode 100644 src/plette/models/scripts.py delete mode 100644 src/plette/models/sections.py delete mode 100644 src/plette/models/sources.py diff --git a/src/plette/models.py b/src/plette/models.py new file mode 100644 index 0000000..d92e7d0 --- /dev/null +++ b/src/plette/models.py @@ -0,0 +1,355 @@ +# pylint: disable=missing-module-docstring,missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member +# pylint: disable=too-few-public-methods +import os +import re +import shlex + + +from dataclasses import dataclass + +from typing import Optional, List, Union + +class ValidationError(ValueError): + pass + + +class BaseModel: + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + +@dataclass +class Hash(BaseModel): + + name: str + value: str + + def validate_name(self, value, field): + if not isinstance(value, str): + raise ValueError("Hash.name must be a string") + + return value + + def validate_value(self, value, field): + if not isinstance(value, str): + raise ValueError("Hash.value must be a string") + + return value + + @classmethod + def from_hash(cls, ins): + """Interpolation to the hash result of `hashlib`. + """ + return cls(name=ins.name, value=ins.hexdigest()) + + @classmethod + def from_dict(cls, value): + """parse a depedency line and create an Hash object""" + try: + name, value = list(value.items())[0] + except AttributeError: + name, value = value.split(":", 1) + return cls(name, value) + + @classmethod + def from_line(cls, value): + """parse a dependecy line and create a Hash object""" + try: + name, value = value.split(":", 1) + except AttributeError: + name, value = list(value.items())[0] + return cls(name, value) + + def __eq__(self, other): + if not isinstance(other, Hash): + raise TypeError(f"cannot compare Hash with {type(other).__name__!r}") + return self.value == other.value + + def as_line(self): + return f"{self.name}:{self.value}" + + +@dataclass +class Source(BaseModel): + """Information on a "simple" Python package index. + + This could be PyPI, or a self-hosted index server, etc. The server + specified by the `url` attribute is expected to provide the "simple" + package API. + """ + name: str + verify_ssl: bool + url: str + + @property + def url_expanded(self): + return os.path.expandvars(self.url) + + def __post_init__(self): + """Run validation methods if declared. + The validation method can be a simple check + that raises ValueError or a transformation to + the field value. + The validation is performed by calling a function named: + `validate_(self, value, field) -> field.type` + """ + for name, field in self.__dataclass_fields__.items(): + if (method := getattr(self, f"validate_{name}", None)): + setattr(self, name, method(getattr(self, name), field=field)) + + def validate_verify_ssl(self, value, field): + if not isinstance(value, bool): + raise ValidationError(f"{field.name}: must be of boolean type") + return value + +@dataclass +class PackageSpecfiers(BaseModel): + + extras: List[str] + + def validate_extras(self, value, field): + if not isinstance(value, list): + raise ValidationError("Extras must be a list") + + +@dataclass +class Package(BaseModel): + + version: Union[Optional[str],Optional[dict]] = "*" + specifiers: Optional[PackageSpecfiers] = None + editable: Optional[bool] = None + extras: Optional[PackageSpecfiers] = None + path: Optional[str] = None + + def validate_extras(self, value, field): + if value is None: + return value + if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): + raise ValidationError("Extras must be a list or None") + return value + + def validate_version(self, value, field): + if isinstance(value, dict): + return value + if isinstance(value, str): + return value + if value is None: + return "*" + + raise ValidationError(f"Unknown type {type(value)} for version") + + def validate_extras(self, value, field): + if value is None: + return value + if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): + raise ValidationError("Extras must be a list or None") + return value + + +@dataclass(init=False) +class Script(BaseModel): + + script: Union[str, List[str]] + + def __init__(self, script): + + if isinstance(script, str): + script = shlex.split(script) + self._parts = [script[0]] + self._parts.extend(script[1:]) + + def validate_script(self, value): + if not (isinstance(value, str) or \ + (isinstance(value, list) and all(isinstance(i, str) for i in value)) + ): + raise ValueError("script must be a string or a list of strings") + + def __repr__(self): + return "Script({0!r})".format(self._parts) + + @property + def command(self): + return self._parts[0] + + @property + def args(self): + return self._parts[1:] + + def cmdify(self, extra_args=None): + """Encode into a cmd-executable string. + + This re-implements CreateProcess's quoting logic to turn a list of + arguments into one single string for the shell to interpret. + + * All double quotes are escaped with a backslash. + * Existing backslashes before a quote are doubled, so they are all + escaped properly. + * Backslashes elsewhere are left as-is; cmd will interpret them + literally. + + The result is then quoted into a pair of double quotes to be grouped. + + An argument is intentionally not quoted if it does not contain + whitespaces. This is done to be compatible with Windows built-in + commands that don't work well with quotes, e.g. everything with `echo`, + and DOS-style (forward slash) switches. + + The intended use of this function is to pre-process an argument list + before passing it into ``subprocess.Popen(..., shell=True)``. + + See also: https://docs.python.org/3/library/subprocess.html + """ + parts = list(self._parts) + if extra_args: + parts.extend(extra_args) + return " ".join( + arg if not next(re.finditer(r'\s', arg), None) + else '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', arg)) + for arg in parts + ) + +@dataclass +class PackageCollection(BaseModel): + packages: List[Package] + + def validate_packages(self, value, field): + if isinstance(value, dict): + packages = {} + for k, v in value.items(): + if isinstance(v, dict): + packages[k] = Package(**v) + else: + packages[k] = Package(version=v) + return packages + + +@dataclass +class ScriptCollection(BaseModel): + scripts: List[Script] + + +@dataclass +class SourceCollection(BaseModel): + + sources: List[Source] + + def validate_sources(self, value, field): + sources = [] + for v in value: + if isinstance(v, dict): + sources.append(Source(**v)) + elif isinstance(v, Source): + sources.append(v) + return sources + + def __iter__(self): + return (d for d in self.sources) + + def __getitem__(self, key): + if isinstance(key, slice): + return SourceCollection(self.sources[key]) + if isinstance(key, int): + src = self.sources[key] + if isinstance(src, dict): + return Source(**key) + if isinstance(src, Source): + return src + raise TypeError(f"Unextepcted type {type(src)}") + + def __len__(self): + return len(self.sources) + + def __setitem__(self, key, value): + if isinstance(key, slice): + self.sources[key] = value + elif isinstance(value, Source): + self.sources.append(value) + elif isinstance(value, list): + self.sources.extend(value) + else: + raise TypeError(f"Unextepcted type {type(value)} for {value}") + + def __delitem__(self, key): + del self.sources[key] + + +@dataclass +class Requires(BaseModel): + python_version: Optional[str] = None + python_full_version: Optional[str] = None + + + +META_SECTIONS = { + "hash": Hash, + "requires": Requires, + "sources": SourceCollection, +} + + +@dataclass +class PipfileSection(BaseModel): + + """ + Dummy pipfile validator that needs to be completed in a future PR + Hint: many pipfile features are undocumented in pipenv/project.py + """ + + +@dataclass +class Meta(BaseModel): + + hash: Hash + pipfile_spec: str + requires: Requires + sources: SourceCollection + + @classmethod + def from_dict(cls, d: dict) -> "Meta": + return cls(**{k.replace('-', '_'): v for k, v in d.items()}) + + def validate_hash(self, value, field): + try: + return Hash(**value) + except TypeError: + return Hash.from_line(value) + + def validate_requires(self, value, field): + return Requires(value) + + def validate_sources(self, value, field): + return SourceCollection(value) + + def validate_pipfile_spec(self, value, field): + if int(value) != 6: + raise ValueError('Only pipefile-spec version 6 is supported') + return value + + +@dataclass +class Pipenv(BaseModel): + """Represent the [pipenv] section in Pipfile""" + allow_prereleases: Optional[bool] = False + install_search_all_sources: Optional[bool] = True + + def validate_allow_prereleases(self, value, field): + if not isinstance(value, bool): + raise ValidationError('allow_prereleases must be a boolean') + return value + + def validate_install_search_all_sources(self, value, field): + if not isinstance(value, bool): + raise ValidationError('install_search_all_sources must be a boolean') + + return value diff --git a/src/plette/models/__init__.py b/src/plette/models/__init__.py deleted file mode 100644 index 8bdb49b..0000000 --- a/src/plette/models/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -__all__ = [ - "DataView", "DataViewCollection", "DataViewMapping", "DataViewSequence", - "validate", "ValidationError", - "Hash", "Package", "Requires", "Source", "Script", - "Meta", "PackageCollection", "ScriptCollection", "SourceCollection", -] - -from .hashes import Hash -from .packages import Package -from .scripts import Script -from .sources import Source - -from .sections import ( - Meta, - Requires, - PackageCollection, - Pipenv, - PipfileSection, - ScriptCollection, - SourceCollection, -) diff --git a/src/plette/models/base.py b/src/plette/models/base.py deleted file mode 100644 index eb04985..0000000 --- a/src/plette/models/base.py +++ /dev/null @@ -1,4 +0,0 @@ -from dataclasses import dataclass - -class ValidationError(ValueError): - pass diff --git a/src/plette/models/hashes.py b/src/plette/models/hashes.py deleted file mode 100644 index 016f9a3..0000000 --- a/src/plette/models/hashes.py +++ /dev/null @@ -1,66 +0,0 @@ -# pylint: disable=missing-module-docstring,missing-class-docstring -# pylint: disable=missing-function-docstring -# pylint: disable=no-member -from dataclasses import dataclass - - -@dataclass -class Hash: - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - name: str - value: str - - def validate_name(self, value, field): - if not isinstance(value, str): - raise ValueError("Hash.name must be a string") - - return value - - def validate_value(self, value, field): - if not isinstance(value, str): - raise ValueError("Hash.value must be a string") - - return value - - @classmethod - def from_hash(cls, ins): - """Interpolation to the hash result of `hashlib`. - """ - return cls(name=ins.name, value=ins.hexdigest()) - - @classmethod - def from_dict(cls, value): - """parse a depedency line and create an Hash object""" - try: - name, value = list(value.items())[0] - except AttributeError: - name, value = value.split(":", 1) - return cls(name, value) - - @classmethod - def from_line(cls, value): - """parse a dependecy line and create a Hash object""" - try: - name, value = value.split(":", 1) - except AttributeError: - name, value = list(value.items())[0] - return cls(name, value) - - def __eq__(self, other): - if not isinstance(other, Hash): - raise TypeError(f"cannot compare Hash with {type(other).__name__!r}") - return self.value == other.value - - def as_line(self): - return f"{self.name}:{self.value}" diff --git a/src/plette/models/packages.py b/src/plette/models/packages.py deleted file mode 100644 index 6ed463e..0000000 --- a/src/plette/models/packages.py +++ /dev/null @@ -1,74 +0,0 @@ -# pylint: disable=missing-module-docstring,missing-class-docstring -# pylint: disable=missing-function-docstring -# pylint: disable=no-member - -from dataclasses import dataclass -from typing import Optional, List, Union - -from .base import ValidationError - -@dataclass -class PackageSpecfiers: - extras: List[str] - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - def validate_extras(self, value, field): - if not isinstance(value, list): - raise ValidationError("Extras must be a list") - - -@dataclass -class Package: - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - version: Union[Optional[str],Optional[dict]] = "*" - specifiers: Optional[PackageSpecfiers] = None - editable: Optional[bool] = None - extras: Optional[PackageSpecfiers] = None - path: Optional[str] = None - - def validate_extras(self, value, field): - if value is None: - return value - if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): - raise ValidationError("Extras must be a list or None") - return value - - def validate_version(self, value, field): - if isinstance(value, dict): - return value - if isinstance(value, str): - return value - if value is None: - return "*" - - raise ValidationError(f"Unknown type {type(value)} for version") - - def validate_extras(self, value, field): - if value is None: - return value - if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): - raise ValidationError("Extras must be a list or None") - return value diff --git a/src/plette/models/scripts.py b/src/plette/models/scripts.py deleted file mode 100644 index ffb3e1c..0000000 --- a/src/plette/models/scripts.py +++ /dev/null @@ -1,77 +0,0 @@ -# pylint: disable=missing-module-docstring,missing-class-docstring -# pylint: disable=missing-function-docstring -# pylint: disable=no-member -import re -import shlex - - -from dataclasses import dataclass -from typing import List, Union - - -@dataclass(init=False) -class Script: - - def __post_init__(self): - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - script: Union[str, List[str]] - - def __init__(self, script): - - if isinstance(script, str): - script = shlex.split(script) - self._parts = [script[0]] - self._parts.extend(script[1:]) - - def validate_script(self, value): - if not (isinstance(value, str) or \ - (isinstance(value, list) and all(isinstance(i, str) for i in value)) - ): - raise ValueError("script must be a string or a list of strings") - - def __repr__(self): - return "Script({0!r})".format(self._parts) - - @property - def command(self): - return self._parts[0] - - @property - def args(self): - return self._parts[1:] - - def cmdify(self, extra_args=None): - """Encode into a cmd-executable string. - - This re-implements CreateProcess's quoting logic to turn a list of - arguments into one single string for the shell to interpret. - - * All double quotes are escaped with a backslash. - * Existing backslashes before a quote are doubled, so they are all - escaped properly. - * Backslashes elsewhere are left as-is; cmd will interpret them - literally. - - The result is then quoted into a pair of double quotes to be grouped. - - An argument is intentionally not quoted if it does not contain - whitespaces. This is done to be compatible with Windows built-in - commands that don't work well with quotes, e.g. everything with `echo`, - and DOS-style (forward slash) switches. - - The intended use of this function is to pre-process an argument list - before passing it into ``subprocess.Popen(..., shell=True)``. - - See also: https://docs.python.org/3/library/subprocess.html - """ - parts = list(self._parts) - if extra_args: - parts.extend(extra_args) - return " ".join( - arg if not next(re.finditer(r'\s', arg), None) - else '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', arg)) - for arg in parts - ) diff --git a/src/plette/models/sections.py b/src/plette/models/sections.py deleted file mode 100644 index 91c3116..0000000 --- a/src/plette/models/sections.py +++ /dev/null @@ -1,206 +0,0 @@ -# pylint: disable=missing-module-docstring,missing-class-docstring -# pylint: disable=missing-function-docstring -# pylint: disable=no-member - -from dataclasses import dataclass -from typing import Optional, List - -from .base import ValidationError -from .hashes import Hash -from .packages import Package -from .scripts import Script -from .sources import Source - - -@dataclass -class PackageCollection: - packages: List[Package] - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - def validate_packages(self, value, field): - if isinstance(value, dict): - packages = {} - for k, v in value.items(): - if isinstance(v, dict): - packages[k] = Package(**v) - else: - packages[k] = Package(version=v) - return packages - - -@dataclass -class ScriptCollection: - scripts: List[Script] - -@dataclass -class SourceCollection: - - sources: List[Source] - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - def validate_sources(self, value, field): - sources = [] - for v in value: - if isinstance(v, dict): - sources.append(Source(**v)) - elif isinstance(v, Source): - sources.append(v) - return sources - - def __iter__(self): - return (d for d in self.sources) - - def __getitem__(self, key): - if isinstance(key, slice): - return SourceCollection(self.sources[key]) - if isinstance(key, int): - src = self.sources[key] - if isinstance(src, dict): - return Source(**key) - if isinstance(src, Source): - return src - raise TypeError(f"Unextepcted type {type(src)}") - - def __len__(self): - return len(self.sources) - - def __setitem__(self, key, value): - if isinstance(key, slice): - self.sources[key] = value - elif isinstance(value, Source): - self.sources.append(value) - elif isinstance(value, list): - self.sources.extend(value) - else: - raise TypeError(f"Unextepcted type {type(value)} for {value}") - - def __delitem__(self, key): - del self.sources[key] - - -@dataclass -class Requires: - python_version: Optional[str] = None - python_full_version: Optional[str] = None - - - -META_SECTIONS = { - "hash": Hash, - "requires": Requires, - "sources": SourceCollection, -} - - -@dataclass -class PipfileSection: - - """ - Dummy pipfile validator that needs to be completed in a future PR - Hint: many pipfile features are undocumented in pipenv/project.py - """ - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - -@dataclass -class Meta: - - hash: Hash - pipfile_spec: str - requires: Requires - sources: SourceCollection - - @classmethod - def from_dict(cls, d: dict) -> "Meta": - return cls(**{k.replace('-', '_'): v for k, v in d.items()}) - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - def validate_hash(self, value, field): - try: - return Hash(**value) - except TypeError: - return Hash.from_line(value) - - def validate_requires(self, value, field): - return Requires(value) - - def validate_sources(self, value, field): - return SourceCollection(value) - - def validate_pipfile_spec(self, value, field): - if int(value) != 6: - raise ValueError('Only pipefile-spec version 6 is supported') - return value - - -@dataclass -class Pipenv: - """Represent the [pipenv] section in Pipfile""" - allow_prereleases: Optional[bool] = False - install_search_all_sources: Optional[bool] = True - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - def validate_allow_prereleases(self, value, field): - if not isinstance(value, bool): - raise ValidationError('allow_prereleases must be a boolean') - return value - - def validate_install_search_all_sources(self, value, field): - if not isinstance(value, bool): - raise ValidationError('install_search_all_sources must be a boolean') - - return value diff --git a/src/plette/models/sources.py b/src/plette/models/sources.py deleted file mode 100644 index e41f61a..0000000 --- a/src/plette/models/sources.py +++ /dev/null @@ -1,44 +0,0 @@ -# pylint: disable=missing-docstring -# pylint: disable=too-few-public-methods - -import os - - -from dataclasses import dataclass -from typing import Optional - -from .base import ValidationError - - -@dataclass -class Source: - """Information on a "simple" Python package index. - - This could be PyPI, or a self-hosted index server, etc. The server - specified by the `url` attribute is expected to provide the "simple" - package API. - """ - name: str - verify_ssl: bool - url: str - - @property - def url_expanded(self): - return os.path.expandvars(self.url) - - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - - def validate_verify_ssl(self, value, field): - if not isinstance(value, bool): - raise ValidationError(f"{field.name}: must be of boolean type") - return value diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 2aa5735..8354c41 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -1,11 +1,13 @@ import hashlib import json +from dataclasses import dataclass, asdict + +from typing import Optional + import tomlkit -from dataclasses import dataclass, field, asdict -from typing import Optional from .models import ( Hash, Requires, PipfileSection, Pipenv, PackageCollection, ScriptCollection, SourceCollection, diff --git a/tests/integration/test_examples.py b/tests/integration/test_examples.py index 45fa6ce..090ef11 100644 --- a/tests/integration/test_examples.py +++ b/tests/integration/test_examples.py @@ -11,7 +11,7 @@ @pytest.mark.parametrize("fname", invalid_files) def test_invalid_files(fname): - with pytest.raises(plette.models.base.ValidationError): + with pytest.raises(plette.models.ValidationError): with open(fname) as f: pipfile = Pipfile.load(f) diff --git a/tests/test_models_hash.py b/tests/test_models_hash.py index 9a14131..99b4f8b 100644 --- a/tests/test_models_hash.py +++ b/tests/test_models_hash.py @@ -1,6 +1,6 @@ import hashlib -from plette.models.hashes import Hash +from plette.models import Hash def test_hash_from_hash(): v = hashlib.md5(b"foo") diff --git a/tests/test_models_sources.py b/tests/test_models_sources.py index 9892290..f0ae440 100644 --- a/tests/test_models_sources.py +++ b/tests/test_models_sources.py @@ -1,5 +1,5 @@ import pytest -from plette.models.sources import Source +from plette.models import Source from plette import models def test_source_from_data(): @@ -51,7 +51,7 @@ def test_validation_error(): data["url"] = "http://localhost:8000" - with pytest.raises(models.base.ValidationError) as exc_info: + with pytest.raises(models.ValidationError) as exc_info: Source(**data) error_message = str(exc_info.value) From 3cb15c6dfc757141c0a5935587fae09e8fb3a00e Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 14:51:21 +0100 Subject: [PATCH 46/59] Address some linting issues --- pylintrc | 3 ++- src/plette/models.py | 24 ++++++++---------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/pylintrc b/pylintrc index b260b33..b8c9fb3 100644 --- a/pylintrc +++ b/pylintrc @@ -192,7 +192,8 @@ good-names=i, ex, Run, _, - fi + fi, + d # Good variable names regexes, separated by a comma. If names match any regex, # they will always be accepted diff --git a/src/plette/models.py b/src/plette/models.py index d92e7d0..888019b 100644 --- a/src/plette/models.py +++ b/src/plette/models.py @@ -11,6 +11,7 @@ from typing import Optional, List, Union + class ValidationError(ValueError): pass @@ -29,6 +30,7 @@ def __post_init__(self): if (method := getattr(self, f"validate_{name}", None)): setattr(self, name, method(getattr(self, name), field=field)) + @dataclass class Hash(BaseModel): @@ -96,23 +98,12 @@ class Source(BaseModel): def url_expanded(self): return os.path.expandvars(self.url) - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - def validate_verify_ssl(self, value, field): if not isinstance(value, bool): raise ValidationError(f"{field.name}: must be of boolean type") return value + @dataclass class PackageSpecfiers(BaseModel): @@ -170,13 +161,13 @@ def __init__(self, script): self._parts.extend(script[1:]) def validate_script(self, value): - if not (isinstance(value, str) or \ + if not (isinstance(value, str) or (isinstance(value, list) and all(isinstance(i, str) for i in value)) ): raise ValueError("script must be a string or a list of strings") def __repr__(self): - return "Script({0!r})".format(self._parts) + return f"Script({self._parts!r})" @property def command(self): @@ -219,6 +210,7 @@ def cmdify(self, extra_args=None): for arg in parts ) + @dataclass class PackageCollection(BaseModel): packages: List[Package] @@ -232,6 +224,7 @@ def validate_packages(self, value, field): else: packages[k] = Package(version=v) return packages + raise ValidationError("Packages must be a dict or a Package instance") @dataclass @@ -290,7 +283,6 @@ class Requires(BaseModel): python_full_version: Optional[str] = None - META_SECTIONS = { "hash": Hash, "requires": Requires, @@ -303,7 +295,7 @@ class PipfileSection(BaseModel): """ Dummy pipfile validator that needs to be completed in a future PR - Hint: many pipfile features are undocumented in pipenv/project.py + Hint: many pipfile features are undocumented in pipenv/project.py """ From c025e85d68239404c1b4c46b31fabdef1d844fab Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 14:51:56 +0100 Subject: [PATCH 47/59] Update the docs --- docs/nested.rst | 67 ++++++++++++++++++++++----------------------- docs/validation.rst | 54 +----------------------------------- 2 files changed, 34 insertions(+), 87 deletions(-) diff --git a/docs/nested.rst b/docs/nested.rst index f7390c3..d90f256 100644 --- a/docs/nested.rst +++ b/docs/nested.rst @@ -12,46 +12,45 @@ Pipfile and Pipfile.lock, such as individual entries in ``[packages]``, ``[dev-packages]``, and ``lockfile['_meta']``. -The Data View -============= +Base Model +=========== Every non-scalar value you get from Plette (e.g. sequence, mapping) is -represented as a `DataView`, or one of its subclasses. This class is simply a -wrapper around the basic collection class, and you can access the underlying -data structure via the ``_data`` attribute:: +represented inherits from `models.BaseModel`, which is a Python `dataclass`:: >>> import plette.models - >>> source = plette.models.Source({ + >>> source = plette.models.Source(**{ ... 'name': 'pypi', ... 'url': 'https://pypi.org/simple', ... 'verify_ssl': True, ... }) ... - >>> source._data - {'name': 'pypi', 'url': 'https://pypi.org/simple', 'verify_ssl': True} - - -Data View Collections -===================== - -There are two special collection classes, ``DataViewMapping`` and -``DataViewSequence``, that hold homogeneous ``DataView`` members. They are -also simply wrappers to ``dict`` and ``list``, respectively, but have specially -implemented magic methods to automatically coerce contained data into a -``DataView`` subclass:: - - >>> sources = plette.models.SourceCollection([source._data]) - >>> sources._data - [{'name': 'pypi', 'url': 'https://pypi.org/simple', 'verify_ssl': True}] - >>> type(sources[0]) - - >>> sources[0] == source - True - >>> sources[0] = { - ... 'name': 'devpi', - ... 'url': 'http://localhost/simple', - ... 'verify_ssl': True, - ... } - ... - >>> sources._data - [{'name': 'devpi', 'url': 'http://localhost/simple', 'verify_ssl': True}] + >>> source + Source(name='pypi', verify_ssl=True, url='https://pypi.org/simple') + + +Collections +=========== + +There a few special collection classes, which can be I dentified by the +suffix ``Collection`` or ``Specifiers``. +They group attributes and behave like ``list`` or ``mappings``. +These classes accept a list of dictionaries as input, +and convert them to the correct object type:: + + >>> SourceCollection([{'name': 'r-pi', 'url': '192.168.1.129:8000', 'verify_ssl': False}, {'name': 'pypi', 'url': 'https://pypi.org/simple', 'verify_ssl': True}]) + SourceCollection(sources=[Source(name='r-pi', verify_ssl=False, url='192.168.1.129:8000'), Source(name='pypi', verify_ssl=True, url='https://pypi.org/simple')]) + + +In addition, they can also accept a list of items of the correct type:: + + >>> rpi = models.Source(**{'name': 'r-pi', 'url': '192.168.1.129:8000', 'verify_ssl': False}) + >>> pypi = models.Source(**{'name': 'pypi', 'url': 'https://pypi.org/simple', 'verify_ssl': True}) + >>> SourceCollection([rpi, pypi]) + SourceCollection(sources=[Source(name='r-pi', verify_ssl=False, url='192.168.1.129:8000'), Source(name='pypi', verify_ssl=True, url='https://pypi.org/simple')]) + +They can also be indexed by name, and can be iterated over:: + + >>> sc = SourceCollection([{'name': 'r-pi', 'url': '192.168.1.129:8000', 'verify_ssl': False}, {'name': 'pypi', 'url': 'https://pypi.org/simple', 'verify_ssl': True}]) + >>> sc[0] + Source(name='r-pi', verify_ssl=False, url='192.168.1.129:8000') diff --git a/docs/validation.rst b/docs/validation.rst index e885e9a..9ecf9a9 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -2,56 +2,4 @@ Validating Data =============== -Plette provides optional validation for input data. This chapter discusses -how validation works. - - -Setting up Validation -===================== - -Validation is provided by the Cerberus_ library. You can install it along with -Plette manually, or by specifying the “validation” extra when installing -Plette: - -.. code-block:: none - - pip install plette[validation] - -Plette automatically enables validation when Cerberus is available. - -.. _Cerberus: http://docs.python-cerberus.org/ - - -Validating Data -=============== - -Data is validated on input (or when a model is loaded). ``ValidationError`` is -raised when validation fails:: - - >>> plette.models.Source({}) - Traceback (most recent call last): - File "", line 1, in - File "plette/models/base.py", line 37, in __init__ - self.validate(data) - File "plette/models/base.py", line 67, in validate - return validate(cls, data) - File "plette/models/base.py", line 27, in validate - raise ValidationError(data, v) - plette.models.base.ValidationError: {} - -This exception class has a ``validator`` member to allow you to access the -underlying Cerberus validator, so you can know what exactly went wrong:: - - >>> try: - ... plette.models.Source({'verify_ssl': True}) - ... except plette.models.ValidationError as e: - ... for error in e.validator._errors: - ... print(error.schema_path) - ... - ('name', 'required') - ('url', 'required') - -See `Ceberus’s error handling documentation`_ to know how the errors are -represented and reported. - -.. _`Ceberus’s error handling documentation`: http://docs.python-cerberus.org/en/stable/errors.html +Validation is no longer an optional feature. All data models are validated. From ba0a7c789c922b716624021e9ea72eb11d9ca85c Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 15:01:48 +0100 Subject: [PATCH 48/59] Remove support for python3.7 add python3.12 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0097a6b..ae6c98b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", 3.11-dev] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] os: [ubuntu-latest] steps: From 72eca464f0cc0fdc91fe0651e0aab07e2aea1652 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 15:03:57 +0100 Subject: [PATCH 49/59] Remove duplicate method --- src/plette/models.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/plette/models.py b/src/plette/models.py index 888019b..c33f1e9 100644 --- a/src/plette/models.py +++ b/src/plette/models.py @@ -140,13 +140,6 @@ def validate_version(self, value, field): raise ValidationError(f"Unknown type {type(value)} for version") - def validate_extras(self, value, field): - if value is None: - return value - if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): - raise ValidationError("Extras must be a list or None") - return value - @dataclass(init=False) class Script(BaseModel): From 3a1f17b8cde4d9ea2c30809ae48ece94b2421a2d Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 15:15:14 +0100 Subject: [PATCH 50/59] lockfiles: address some pylint issues --- src/plette/lockfiles.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index cc66be3..f52fcde 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -11,7 +11,10 @@ from dataclasses import dataclass, field, asdict from typing import Optional -from .models import Meta, PackageCollection, Package +from .models import BaseModel, Meta, PackageCollection, Package + +PIPFILE_SPEC_CURRENT = 6 + def flatten_versions(d): copy = {} @@ -24,6 +27,7 @@ def flatten_versions(d): copy[key] = value["version"] return copy + def remove_empty_values(d): # Iterate over a copy of the dictionary for key, value in list(d.items()): @@ -37,6 +41,7 @@ def remove_empty_values(d): elif value is None or value == '': del d[key] + class DCJSONEncoder(json.JSONEncoder): def default(self, o): if dataclasses.is_dataclass(o): @@ -61,8 +66,6 @@ def default(self, o): return o return super().default(o) -PIPFILE_SPEC_CURRENT = 6 - def _copy_jsonsafe(value): """Deep-copy a value into JSON-safe types. @@ -79,8 +82,9 @@ def _copy_jsonsafe(value): @dataclass -class Lockfile: +class Lockfile(BaseModel): """Representation of a Pipfile.lock.""" + _meta: field(init=False) default: Optional[dict] = field(default_factory=dict) develop: Optional[dict] = field(default_factory=dict) @@ -156,15 +160,14 @@ def __getitem__(self, key): try: if key == "_meta": return Meta(value) - else: - return PackageCollection(value) + return PackageCollection(value) except KeyError: return value def is_up_to_date(self, pipfile): return self.meta.hash == pipfile.get_hash() - def dump(self, fh, encoding=None): + def dump(self, fh): json.dump(self, fh, cls=DCJSONEncoder) self.meta = self._meta From 2b0767d25cdadcb3e256cbd10ad9c9e664ee292f Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 15:16:03 +0100 Subject: [PATCH 51/59] Fix bug introduced because of wrong validation --- src/plette/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plette/models.py b/src/plette/models.py index c33f1e9..50a1dfd 100644 --- a/src/plette/models.py +++ b/src/plette/models.py @@ -206,6 +206,7 @@ def cmdify(self, extra_args=None): @dataclass class PackageCollection(BaseModel): + packages: List[Package] def validate_packages(self, value, field): @@ -217,7 +218,7 @@ def validate_packages(self, value, field): else: packages[k] = Package(version=v) return packages - raise ValidationError("Packages must be a dict or a Package instance") + return value @dataclass From 58bb217a749aa9e8890e7f46ba78294330e64b07 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 15:16:27 +0100 Subject: [PATCH 52/59] Inherit Pipefile class from BaseModel --- src/plette/pipfiles.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 8354c41..1f44e64 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -9,10 +9,12 @@ from .models import ( + BaseModel, Hash, Requires, PipfileSection, Pipenv, PackageCollection, ScriptCollection, SourceCollection, ) + def remove_empty_values(d): # Iterate over a copy of the dictionary for key, value in list(d.items()): @@ -26,6 +28,7 @@ def remove_empty_values(d): elif value is None or value == '': del d[key] + PIPFILE_SECTIONS = { "sources": SourceCollection, "packages": PackageCollection, @@ -43,8 +46,9 @@ def remove_empty_values(d): verify_ssl = true """ + @dataclass -class Pipfile: +class Pipfile(BaseModel): """Representation of a Pipfile.""" sources: SourceCollection packages: Optional[PackageCollection] = None @@ -55,19 +59,6 @@ class Pipfile: pipfile: Optional[PipfileSection] = None pipenv: Optional[Pipenv] = None - def __post_init__(self): - """Run validation methods if declared. - The validation method can be a simple check - that raises ValueError or a transformation to - the field value. - The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` - """ - - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - def validate_sources(self, value, field): if isinstance(value, list): return SourceCollection(value) @@ -76,6 +67,7 @@ def validate_sources(self, value, field): def validate_pipenv(self, value, field): if value is not None: return Pipenv(**value) + return value def validate_packages(self, value, field): PackageCollection(value) @@ -91,7 +83,8 @@ def to_dict(self): } data["_meta"].update(asdict(getattr(self, "sources", {}))) for category, values in self.__dict__.items(): - if category in PIPFILE_SECTIONS or category in ("default", "develop", "pipenv"): + if category in PIPFILE_SECTIONS or category in ( + "default", "develop", "pipenv"): continue data[category] = values remove_empty_values(data) From 54419adb7dbbc395771cadcce58cb0a8e9d54170 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 15:37:51 +0100 Subject: [PATCH 53/59] Remove redundant comment --- src/plette/pipfiles.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 1f44e64..473d9b5 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -140,5 +140,3 @@ def dump(self, f, encoding=None): if encoding is not None: content = content.encode(encoding) f.write(content) - - # todo add a method make pipfile behave like a dict so dump works From 1693b410e1006388ec07034cfef303e0ba69d5f7 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 20:02:26 +0100 Subject: [PATCH 54/59] Remove redundant code in lockfiles --- src/plette/lockfiles.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index f52fcde..4ba9266 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -85,7 +85,7 @@ def _copy_jsonsafe(value): class Lockfile(BaseModel): """Representation of a Pipfile.lock.""" - _meta: field(init=False) + _meta: Optional[Meta] default: Optional[dict] = field(default_factory=dict) develop: Optional[dict] = field(default_factory=dict) @@ -97,11 +97,7 @@ def __post_init__(self): The validation is performed by calling a function named: `validate_(self, value, field) -> field.type` """ - - for name, field in self.__dataclass_fields__.items(): - if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) - + super().__post_init__() self.meta = self._meta def validate__meta(self, value, field): From 08b8545cb45e282b3c03367dfd07ba9d1ae63ee2 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 20:03:00 +0100 Subject: [PATCH 55/59] Remove unused field from all functions --- src/plette/lockfiles.py | 2 +- src/plette/models.py | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index 4ba9266..33414ea 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -110,7 +110,7 @@ def validate_meta(self, value, field): value['pipfile_spec'] = value.pop('pipfile-spec') return Meta(**value) - def validate_default(self, value, field): + def validate_default(self, value): packages = {} for name, spec in value.items(): packages[name] = Package(spec) diff --git a/src/plette/models.py b/src/plette/models.py index 50a1dfd..091d361 100644 --- a/src/plette/models.py +++ b/src/plette/models.py @@ -26,9 +26,9 @@ def __post_init__(self): The validation is performed by calling a function named: `validate_(self, value, field) -> field.type` """ - for name, field in self.__dataclass_fields__.items(): + for name, _ in self.__dataclass_fields__.items(): if (method := getattr(self, f"validate_{name}", None)): - setattr(self, name, method(getattr(self, name), field=field)) + setattr(self, name, method(getattr(self, name))) @dataclass @@ -37,13 +37,13 @@ class Hash(BaseModel): name: str value: str - def validate_name(self, value, field): + def validate_name(self, value): if not isinstance(value, str): raise ValueError("Hash.name must be a string") return value - def validate_value(self, value, field): + def validate_value(self, value): if not isinstance(value, str): raise ValueError("Hash.value must be a string") @@ -109,7 +109,7 @@ class PackageSpecfiers(BaseModel): extras: List[str] - def validate_extras(self, value, field): + def validate_extras(self, value): if not isinstance(value, list): raise ValidationError("Extras must be a list") @@ -123,14 +123,14 @@ class Package(BaseModel): extras: Optional[PackageSpecfiers] = None path: Optional[str] = None - def validate_extras(self, value, field): + def validate_extras(self, value): if value is None: return value if not (isinstance(value, list) and all(isinstance(i, str) for i in value)): raise ValidationError("Extras must be a list or None") return value - def validate_version(self, value, field): + def validate_version(self, value): if isinstance(value, dict): return value if isinstance(value, str): @@ -209,7 +209,7 @@ class PackageCollection(BaseModel): packages: List[Package] - def validate_packages(self, value, field): + def validate_packages(self, value): if isinstance(value, dict): packages = {} for k, v in value.items(): @@ -231,7 +231,7 @@ class SourceCollection(BaseModel): sources: List[Source] - def validate_sources(self, value, field): + def validate_sources(self, value): sources = [] for v in value: if isinstance(v, dict): @@ -305,19 +305,19 @@ class Meta(BaseModel): def from_dict(cls, d: dict) -> "Meta": return cls(**{k.replace('-', '_'): v for k, v in d.items()}) - def validate_hash(self, value, field): + def validate_hash(self, value): try: return Hash(**value) except TypeError: return Hash.from_line(value) - def validate_requires(self, value, field): + def validate_requires(self, value): return Requires(value) - def validate_sources(self, value, field): + def validate_sources(self, value): return SourceCollection(value) - def validate_pipfile_spec(self, value, field): + def validate_pipfile_spec(self, value): if int(value) != 6: raise ValueError('Only pipefile-spec version 6 is supported') return value @@ -329,12 +329,12 @@ class Pipenv(BaseModel): allow_prereleases: Optional[bool] = False install_search_all_sources: Optional[bool] = True - def validate_allow_prereleases(self, value, field): + def validate_allow_prereleases(self, value): if not isinstance(value, bool): raise ValidationError('allow_prereleases must be a boolean') return value - def validate_install_search_all_sources(self, value, field): + def validate_install_search_all_sources(self, value): if not isinstance(value, bool): raise ValidationError('install_search_all_sources must be a boolean') From b60109d3806ec0c65cf8d6076790f33a5d9d90e4 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 20:03:21 +0100 Subject: [PATCH 56/59] Address some pylint warnings --- src/plette/lockfiles.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index 33414ea..f060da1 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -133,20 +133,23 @@ def with_meta_from(cls, pipfile, categories=None): "requires": _copy_jsonsafe(getattr(pipfile, "requires", {})), }, } + data["_meta"].update(asdict(pipfile.sources)) + if categories is None: data["default"] = _copy_jsonsafe(getattr(pipfile, "packages", {})) data["develop"] = _copy_jsonsafe(getattr(pipfile, "dev-packages", {})) else: for category in categories: - if category == "default" or category == "packages": + if category in ["default", "packages"]: data["default"] = _copy_jsonsafe(getattr(pipfile,"packages", {})) - elif category == "develop" or category == "dev-packages": - data["develop"] = _copy_jsonsafe(getattr(pipfile,"dev-packages", {})) + elif category in ["develop", "dev-packages"]: + data["develop"] = _copy_jsonsafe( + getattr(pipfile,"dev-packages", {})) else: data[category] = _copy_jsonsafe(getattr(pipfile, category, {})) if "default" not in data: - data["default"] = {} + data["default"] = {} if "develop" not in data: data["develop"] = {} return cls(data) @@ -155,7 +158,7 @@ def __getitem__(self, key): value = self[key] try: if key == "_meta": - return Meta(value) + return Meta(**value) return PackageCollection(value) except KeyError: return value From 35cf04bb83900cd093b0636f2d4bf7fea0ee50e1 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 20:05:24 +0100 Subject: [PATCH 57/59] Remove unused argument field --- src/plette/lockfiles.py | 8 ++++---- src/plette/pipfiles.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index f060da1..d2f410d 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -95,15 +95,15 @@ def __post_init__(self): that raises ValueError or a transformation to the field value. The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` + `validate_(self, value) -> field.type` """ super().__post_init__() self.meta = self._meta - def validate__meta(self, value, field): - return self.validate_meta(value, field) + def validate__meta(self, value): + return self.validate_meta(value) - def validate_meta(self, value, field): + def validate_meta(self, value): if "_meta" in value: value = value["_meta"] if 'pipfile-spec' in value: diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 473d9b5..9ffd081 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -59,17 +59,17 @@ class Pipfile(BaseModel): pipfile: Optional[PipfileSection] = None pipenv: Optional[Pipenv] = None - def validate_sources(self, value, field): + def validate_sources(self, value): if isinstance(value, list): return SourceCollection(value) return SourceCollection(value.value) - def validate_pipenv(self, value, field): + def validate_pipenv(self, value): if value is not None: return Pipenv(**value) return value - def validate_packages(self, value, field): + def validate_packages(self, value): PackageCollection(value) return value From 9fd0118f1d0a59e814c79e0cfa149865582b0162 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 20:09:49 +0100 Subject: [PATCH 58/59] Remove another usage of field in signature --- src/plette/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plette/models.py b/src/plette/models.py index 091d361..c0ff939 100644 --- a/src/plette/models.py +++ b/src/plette/models.py @@ -24,7 +24,7 @@ def __post_init__(self): that raises ValueError or a transformation to the field value. The validation is performed by calling a function named: - `validate_(self, value, field) -> field.type` + `validate_(self, value) -> field.type` """ for name, _ in self.__dataclass_fields__.items(): if (method := getattr(self, f"validate_{name}", None)): @@ -98,9 +98,9 @@ class Source(BaseModel): def url_expanded(self): return os.path.expandvars(self.url) - def validate_verify_ssl(self, value, field): + def validate_verify_ssl(self, value): if not isinstance(value, bool): - raise ValidationError(f"{field.name}: must be of boolean type") + raise ValidationError("verify_ssl: must be of boolean type") return value From e493f6ad78f26f273621b7c0112b3ffd615749e9 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sun, 10 Dec 2023 20:10:11 +0100 Subject: [PATCH 59/59] Remove duplicate function --- src/plette/lockfiles.py | 19 +------------------ src/plette/models.py | 14 ++++++++++++++ src/plette/pipfiles.py | 15 +-------------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/plette/lockfiles.py b/src/plette/lockfiles.py index d2f410d..f573514 100644 --- a/src/plette/lockfiles.py +++ b/src/plette/lockfiles.py @@ -11,7 +11,7 @@ from dataclasses import dataclass, field, asdict from typing import Optional -from .models import BaseModel, Meta, PackageCollection, Package +from .models import BaseModel, Meta, PackageCollection, Package, remove_empty_values PIPFILE_SPEC_CURRENT = 6 @@ -20,28 +20,11 @@ def flatten_versions(d): copy = {} # Iterate over a copy of the dictionary for key, value in d.items(): - # If the value is a dictionary, call the function recursively - #if isinstance(value, dict): - # flatten_dict(value) # If the key is "version", replace the key with the value copy[key] = value["version"] return copy -def remove_empty_values(d): - # Iterate over a copy of the dictionary - for key, value in list(d.items()): - # If the value is a dictionary, call the function recursively - if isinstance(value, dict): - remove_empty_values(value) - # If the dictionary is empty, remove the key - if not value: - del d[key] - # If the value is None or an empty string, remove the key - elif value is None or value == '': - del d[key] - - class DCJSONEncoder(json.JSONEncoder): def default(self, o): if dataclasses.is_dataclass(o): diff --git a/src/plette/models.py b/src/plette/models.py index c0ff939..db6e6dd 100644 --- a/src/plette/models.py +++ b/src/plette/models.py @@ -16,6 +16,20 @@ class ValidationError(ValueError): pass +def remove_empty_values(d): + # Iterate over a copy of the dictionary + for key, value in list(d.items()): + # If the value is a dictionary, call the function recursively + if isinstance(value, dict): + remove_empty_values(value) + # If the dictionary is empty, remove the key + if not value: + del d[key] + # If the value is None or an empty string, remove the key + elif value is None or value == '': + del d[key] + + class BaseModel: def __post_init__(self): diff --git a/src/plette/pipfiles.py b/src/plette/pipfiles.py index 9ffd081..914741d 100644 --- a/src/plette/pipfiles.py +++ b/src/plette/pipfiles.py @@ -12,23 +12,10 @@ BaseModel, Hash, Requires, PipfileSection, Pipenv, PackageCollection, ScriptCollection, SourceCollection, + remove_empty_values ) -def remove_empty_values(d): - # Iterate over a copy of the dictionary - for key, value in list(d.items()): - # If the value is a dictionary, call the function recursively - if isinstance(value, dict): - remove_empty_values(value) - # If the dictionary is empty, remove the key - if not value: - del d[key] - # If the value is None or an empty string, remove the key - elif value is None or value == '': - del d[key] - - PIPFILE_SECTIONS = { "sources": SourceCollection, "packages": PackageCollection,