pylock.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. from __future__ import annotations
  2. import dataclasses
  3. import logging
  4. import re
  5. from collections.abc import Mapping, Sequence
  6. from dataclasses import dataclass
  7. from datetime import datetime
  8. from typing import (
  9. TYPE_CHECKING,
  10. Any,
  11. Callable,
  12. Protocol,
  13. TypeVar,
  14. )
  15. from .markers import Marker
  16. from .specifiers import SpecifierSet
  17. from .utils import NormalizedName, is_normalized_name
  18. from .version import Version
  19. if TYPE_CHECKING: # pragma: no cover
  20. from pathlib import Path
  21. from typing_extensions import Self
  22. _logger = logging.getLogger(__name__)
  23. __all__ = [
  24. "Package",
  25. "PackageArchive",
  26. "PackageDirectory",
  27. "PackageSdist",
  28. "PackageVcs",
  29. "PackageWheel",
  30. "Pylock",
  31. "PylockUnsupportedVersionError",
  32. "PylockValidationError",
  33. "is_valid_pylock_path",
  34. ]
  35. _T = TypeVar("_T")
  36. _T2 = TypeVar("_T2")
  37. class _FromMappingProtocol(Protocol): # pragma: no cover
  38. @classmethod
  39. def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
  40. _FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
  41. _PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
  42. def is_valid_pylock_path(path: Path) -> bool:
  43. """Check if the given path is a valid pylock file path."""
  44. return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))
  45. def _toml_key(key: str) -> str:
  46. return key.replace("_", "-")
  47. def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401
  48. if isinstance(value, (Version, Marker, SpecifierSet)):
  49. return str(value)
  50. if isinstance(value, Sequence) and key == "environments":
  51. return [str(v) for v in value]
  52. return value
  53. def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
  54. return {
  55. _toml_key(key): _toml_value(key, value)
  56. for key, value in data
  57. if value is not None
  58. }
  59. def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
  60. """Get a value from the dictionary and verify it's the expected type."""
  61. if (value := d.get(key)) is None:
  62. return None
  63. if not isinstance(value, expected_type):
  64. raise PylockValidationError(
  65. f"Unexpected type {type(value).__name__} "
  66. f"(expected {expected_type.__name__})",
  67. context=key,
  68. )
  69. return value
  70. def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
  71. """Get a required value from the dictionary and verify it's the expected type."""
  72. if (value := _get(d, expected_type, key)) is None:
  73. raise _PylockRequiredKeyError(key)
  74. return value
  75. def _get_sequence(
  76. d: Mapping[str, Any], expected_item_type: type[_T], key: str
  77. ) -> Sequence[_T] | None:
  78. """Get a list value from the dictionary and verify it's the expected items type."""
  79. if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]
  80. return None
  81. if isinstance(value, (str, bytes)):
  82. # special case: str and bytes are Sequences, but we want to reject it
  83. raise PylockValidationError(
  84. f"Unexpected type {type(value).__name__} (expected Sequence)",
  85. context=key,
  86. )
  87. for i, item in enumerate(value):
  88. if not isinstance(item, expected_item_type):
  89. raise PylockValidationError(
  90. f"Unexpected type {type(item).__name__} "
  91. f"(expected {expected_item_type.__name__})",
  92. context=f"{key}[{i}]",
  93. )
  94. return value
  95. def _get_as(
  96. d: Mapping[str, Any],
  97. expected_type: type[_T],
  98. target_type: Callable[[_T], _T2],
  99. key: str,
  100. ) -> _T2 | None:
  101. """Get a value from the dictionary, verify it's the expected type,
  102. and convert to the target type.
  103. This assumes the target_type constructor accepts the value.
  104. """
  105. if (value := _get(d, expected_type, key)) is None:
  106. return None
  107. try:
  108. return target_type(value)
  109. except Exception as e:
  110. raise PylockValidationError(e, context=key) from e
  111. def _get_required_as(
  112. d: Mapping[str, Any],
  113. expected_type: type[_T],
  114. target_type: Callable[[_T], _T2],
  115. key: str,
  116. ) -> _T2:
  117. """Get a required value from the dict, verify it's the expected type,
  118. and convert to the target type."""
  119. if (value := _get_as(d, expected_type, target_type, key)) is None:
  120. raise _PylockRequiredKeyError(key)
  121. return value
  122. def _get_sequence_as(
  123. d: Mapping[str, Any],
  124. expected_item_type: type[_T],
  125. target_item_type: Callable[[_T], _T2],
  126. key: str,
  127. ) -> list[_T2] | None:
  128. """Get list value from dictionary and verify expected items type."""
  129. if (value := _get_sequence(d, expected_item_type, key)) is None:
  130. return None
  131. result = []
  132. try:
  133. for item in value:
  134. typed_item = target_item_type(item)
  135. result.append(typed_item)
  136. except Exception as e:
  137. raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
  138. return result
  139. def _get_object(
  140. d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
  141. ) -> _FromMappingProtocolT | None:
  142. """Get a dictionary value from the dictionary and convert it to a dataclass."""
  143. if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
  144. return None
  145. try:
  146. return target_type._from_dict(value)
  147. except Exception as e:
  148. raise PylockValidationError(e, context=key) from e
  149. def _get_sequence_of_objects(
  150. d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
  151. ) -> list[_FromMappingProtocolT] | None:
  152. """Get a list value from the dictionary and convert its items to a dataclass."""
  153. if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]
  154. return None
  155. result: list[_FromMappingProtocolT] = []
  156. try:
  157. for item in value:
  158. typed_item = target_item_type._from_dict(item)
  159. result.append(typed_item)
  160. except Exception as e:
  161. raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
  162. return result
  163. def _get_required_sequence_of_objects(
  164. d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
  165. ) -> Sequence[_FromMappingProtocolT]:
  166. """Get a required list value from the dictionary and convert its items to a
  167. dataclass."""
  168. if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:
  169. raise _PylockRequiredKeyError(key)
  170. return result
  171. def _validate_normalized_name(name: str) -> NormalizedName:
  172. """Validate that a string is a NormalizedName."""
  173. if not is_normalized_name(name):
  174. raise PylockValidationError(f"Name {name!r} is not normalized")
  175. return NormalizedName(name)
  176. def _validate_path_url(path: str | None, url: str | None) -> None:
  177. if not path and not url:
  178. raise PylockValidationError("path or url must be provided")
  179. def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
  180. if not hashes:
  181. raise PylockValidationError("At least one hash must be provided")
  182. if not all(isinstance(hash_val, str) for hash_val in hashes.values()):
  183. raise PylockValidationError("Hash values must be strings")
  184. return hashes
  185. class PylockValidationError(Exception):
  186. """Raised when when input data is not spec-compliant."""
  187. context: str | None = None
  188. message: str
  189. def __init__(
  190. self,
  191. cause: str | Exception,
  192. *,
  193. context: str | None = None,
  194. ) -> None:
  195. if isinstance(cause, PylockValidationError):
  196. if cause.context:
  197. self.context = (
  198. f"{context}.{cause.context}" if context else cause.context
  199. )
  200. else:
  201. self.context = context
  202. self.message = cause.message
  203. else:
  204. self.context = context
  205. self.message = str(cause)
  206. def __str__(self) -> str:
  207. if self.context:
  208. return f"{self.message} in {self.context!r}"
  209. return self.message
  210. class _PylockRequiredKeyError(PylockValidationError):
  211. def __init__(self, key: str) -> None:
  212. super().__init__("Missing required value", context=key)
  213. class PylockUnsupportedVersionError(PylockValidationError):
  214. """Raised when encountering an unsupported `lock_version`."""
  215. @dataclass(frozen=True, init=False)
  216. class PackageVcs:
  217. type: str
  218. url: str | None = None
  219. path: str | None = None
  220. requested_revision: str | None = None
  221. commit_id: str # type: ignore[misc]
  222. subdirectory: str | None = None
  223. def __init__(
  224. self,
  225. *,
  226. type: str,
  227. url: str | None = None,
  228. path: str | None = None,
  229. requested_revision: str | None = None,
  230. commit_id: str,
  231. subdirectory: str | None = None,
  232. ) -> None:
  233. # In Python 3.10+ make dataclass kw_only=True and remove __init__
  234. object.__setattr__(self, "type", type)
  235. object.__setattr__(self, "url", url)
  236. object.__setattr__(self, "path", path)
  237. object.__setattr__(self, "requested_revision", requested_revision)
  238. object.__setattr__(self, "commit_id", commit_id)
  239. object.__setattr__(self, "subdirectory", subdirectory)
  240. @classmethod
  241. def _from_dict(cls, d: Mapping[str, Any]) -> Self:
  242. package_vcs = cls(
  243. type=_get_required(d, str, "type"),
  244. url=_get(d, str, "url"),
  245. path=_get(d, str, "path"),
  246. requested_revision=_get(d, str, "requested-revision"),
  247. commit_id=_get_required(d, str, "commit-id"),
  248. subdirectory=_get(d, str, "subdirectory"),
  249. )
  250. _validate_path_url(package_vcs.path, package_vcs.url)
  251. return package_vcs
  252. @dataclass(frozen=True, init=False)
  253. class PackageDirectory:
  254. path: str
  255. editable: bool | None = None
  256. subdirectory: str | None = None
  257. def __init__(
  258. self,
  259. *,
  260. path: str,
  261. editable: bool | None = None,
  262. subdirectory: str | None = None,
  263. ) -> None:
  264. # In Python 3.10+ make dataclass kw_only=True and remove __init__
  265. object.__setattr__(self, "path", path)
  266. object.__setattr__(self, "editable", editable)
  267. object.__setattr__(self, "subdirectory", subdirectory)
  268. @classmethod
  269. def _from_dict(cls, d: Mapping[str, Any]) -> Self:
  270. return cls(
  271. path=_get_required(d, str, "path"),
  272. editable=_get(d, bool, "editable"),
  273. subdirectory=_get(d, str, "subdirectory"),
  274. )
  275. @dataclass(frozen=True, init=False)
  276. class PackageArchive:
  277. url: str | None = None
  278. path: str | None = None
  279. size: int | None = None
  280. upload_time: datetime | None = None
  281. hashes: Mapping[str, str] # type: ignore[misc]
  282. subdirectory: str | None = None
  283. def __init__(
  284. self,
  285. *,
  286. url: str | None = None,
  287. path: str | None = None,
  288. size: int | None = None,
  289. upload_time: datetime | None = None,
  290. hashes: Mapping[str, str],
  291. subdirectory: str | None = None,
  292. ) -> None:
  293. # In Python 3.10+ make dataclass kw_only=True and remove __init__
  294. object.__setattr__(self, "url", url)
  295. object.__setattr__(self, "path", path)
  296. object.__setattr__(self, "size", size)
  297. object.__setattr__(self, "upload_time", upload_time)
  298. object.__setattr__(self, "hashes", hashes)
  299. object.__setattr__(self, "subdirectory", subdirectory)
  300. @classmethod
  301. def _from_dict(cls, d: Mapping[str, Any]) -> Self:
  302. package_archive = cls(
  303. url=_get(d, str, "url"),
  304. path=_get(d, str, "path"),
  305. size=_get(d, int, "size"),
  306. upload_time=_get(d, datetime, "upload-time"),
  307. hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
  308. subdirectory=_get(d, str, "subdirectory"),
  309. )
  310. _validate_path_url(package_archive.path, package_archive.url)
  311. return package_archive
  312. @dataclass(frozen=True, init=False)
  313. class PackageSdist:
  314. name: str | None = None
  315. upload_time: datetime | None = None
  316. url: str | None = None
  317. path: str | None = None
  318. size: int | None = None
  319. hashes: Mapping[str, str] # type: ignore[misc]
  320. def __init__(
  321. self,
  322. *,
  323. name: str | None = None,
  324. upload_time: datetime | None = None,
  325. url: str | None = None,
  326. path: str | None = None,
  327. size: int | None = None,
  328. hashes: Mapping[str, str],
  329. ) -> None:
  330. # In Python 3.10+ make dataclass kw_only=True and remove __init__
  331. object.__setattr__(self, "name", name)
  332. object.__setattr__(self, "upload_time", upload_time)
  333. object.__setattr__(self, "url", url)
  334. object.__setattr__(self, "path", path)
  335. object.__setattr__(self, "size", size)
  336. object.__setattr__(self, "hashes", hashes)
  337. @classmethod
  338. def _from_dict(cls, d: Mapping[str, Any]) -> Self:
  339. package_sdist = cls(
  340. name=_get(d, str, "name"),
  341. upload_time=_get(d, datetime, "upload-time"),
  342. url=_get(d, str, "url"),
  343. path=_get(d, str, "path"),
  344. size=_get(d, int, "size"),
  345. hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
  346. )
  347. _validate_path_url(package_sdist.path, package_sdist.url)
  348. return package_sdist
  349. @dataclass(frozen=True, init=False)
  350. class PackageWheel:
  351. name: str | None = None
  352. upload_time: datetime | None = None
  353. url: str | None = None
  354. path: str | None = None
  355. size: int | None = None
  356. hashes: Mapping[str, str] # type: ignore[misc]
  357. def __init__(
  358. self,
  359. *,
  360. name: str | None = None,
  361. upload_time: datetime | None = None,
  362. url: str | None = None,
  363. path: str | None = None,
  364. size: int | None = None,
  365. hashes: Mapping[str, str],
  366. ) -> None:
  367. # In Python 3.10+ make dataclass kw_only=True and remove __init__
  368. object.__setattr__(self, "name", name)
  369. object.__setattr__(self, "upload_time", upload_time)
  370. object.__setattr__(self, "url", url)
  371. object.__setattr__(self, "path", path)
  372. object.__setattr__(self, "size", size)
  373. object.__setattr__(self, "hashes", hashes)
  374. @classmethod
  375. def _from_dict(cls, d: Mapping[str, Any]) -> Self:
  376. package_wheel = cls(
  377. name=_get(d, str, "name"),
  378. upload_time=_get(d, datetime, "upload-time"),
  379. url=_get(d, str, "url"),
  380. path=_get(d, str, "path"),
  381. size=_get(d, int, "size"),
  382. hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
  383. )
  384. _validate_path_url(package_wheel.path, package_wheel.url)
  385. return package_wheel
  386. @dataclass(frozen=True, init=False)
  387. class Package:
  388. name: NormalizedName
  389. version: Version | None = None
  390. marker: Marker | None = None
  391. requires_python: SpecifierSet | None = None
  392. dependencies: Sequence[Mapping[str, Any]] | None = None
  393. vcs: PackageVcs | None = None
  394. directory: PackageDirectory | None = None
  395. archive: PackageArchive | None = None
  396. index: str | None = None
  397. sdist: PackageSdist | None = None
  398. wheels: Sequence[PackageWheel] | None = None
  399. attestation_identities: Sequence[Mapping[str, Any]] | None = None
  400. tool: Mapping[str, Any] | None = None
  401. def __init__(
  402. self,
  403. *,
  404. name: NormalizedName,
  405. version: Version | None = None,
  406. marker: Marker | None = None,
  407. requires_python: SpecifierSet | None = None,
  408. dependencies: Sequence[Mapping[str, Any]] | None = None,
  409. vcs: PackageVcs | None = None,
  410. directory: PackageDirectory | None = None,
  411. archive: PackageArchive | None = None,
  412. index: str | None = None,
  413. sdist: PackageSdist | None = None,
  414. wheels: Sequence[PackageWheel] | None = None,
  415. attestation_identities: Sequence[Mapping[str, Any]] | None = None,
  416. tool: Mapping[str, Any] | None = None,
  417. ) -> None:
  418. # In Python 3.10+ make dataclass kw_only=True and remove __init__
  419. object.__setattr__(self, "name", name)
  420. object.__setattr__(self, "version", version)
  421. object.__setattr__(self, "marker", marker)
  422. object.__setattr__(self, "requires_python", requires_python)
  423. object.__setattr__(self, "dependencies", dependencies)
  424. object.__setattr__(self, "vcs", vcs)
  425. object.__setattr__(self, "directory", directory)
  426. object.__setattr__(self, "archive", archive)
  427. object.__setattr__(self, "index", index)
  428. object.__setattr__(self, "sdist", sdist)
  429. object.__setattr__(self, "wheels", wheels)
  430. object.__setattr__(self, "attestation_identities", attestation_identities)
  431. object.__setattr__(self, "tool", tool)
  432. @classmethod
  433. def _from_dict(cls, d: Mapping[str, Any]) -> Self:
  434. package = cls(
  435. name=_get_required_as(d, str, _validate_normalized_name, "name"),
  436. version=_get_as(d, str, Version, "version"),
  437. requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
  438. dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]
  439. marker=_get_as(d, str, Marker, "marker"),
  440. vcs=_get_object(d, PackageVcs, "vcs"),
  441. directory=_get_object(d, PackageDirectory, "directory"),
  442. archive=_get_object(d, PackageArchive, "archive"),
  443. index=_get(d, str, "index"),
  444. sdist=_get_object(d, PackageSdist, "sdist"),
  445. wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
  446. attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]
  447. tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
  448. )
  449. distributions = bool(package.sdist) + len(package.wheels or [])
  450. direct_urls = (
  451. bool(package.vcs) + bool(package.directory) + bool(package.archive)
  452. )
  453. if distributions > 0 and direct_urls > 0:
  454. raise PylockValidationError(
  455. "None of vcs, directory, archive must be set if sdist or wheels are set"
  456. )
  457. if distributions == 0 and direct_urls != 1:
  458. raise PylockValidationError(
  459. "Exactly one of vcs, directory, archive must be set "
  460. "if sdist and wheels are not set"
  461. )
  462. try:
  463. for i, attestation_identity in enumerate( # noqa: B007
  464. package.attestation_identities or []
  465. ):
  466. _get_required(attestation_identity, str, "kind")
  467. except Exception as e:
  468. raise PylockValidationError(
  469. e, context=f"attestation-identities[{i}]"
  470. ) from e
  471. return package
  472. @property
  473. def is_direct(self) -> bool:
  474. return not (self.sdist or self.wheels)
  475. @dataclass(frozen=True, init=False)
  476. class Pylock:
  477. """A class representing a pylock file."""
  478. lock_version: Version
  479. environments: Sequence[Marker] | None = None
  480. requires_python: SpecifierSet | None = None
  481. extras: Sequence[NormalizedName] | None = None
  482. dependency_groups: Sequence[str] | None = None
  483. default_groups: Sequence[str] | None = None
  484. created_by: str # type: ignore[misc]
  485. packages: Sequence[Package] # type: ignore[misc]
  486. tool: Mapping[str, Any] | None = None
  487. def __init__(
  488. self,
  489. *,
  490. lock_version: Version,
  491. environments: Sequence[Marker] | None = None,
  492. requires_python: SpecifierSet | None = None,
  493. extras: Sequence[NormalizedName] | None = None,
  494. dependency_groups: Sequence[str] | None = None,
  495. default_groups: Sequence[str] | None = None,
  496. created_by: str,
  497. packages: Sequence[Package],
  498. tool: Mapping[str, Any] | None = None,
  499. ) -> None:
  500. # In Python 3.10+ make dataclass kw_only=True and remove __init__
  501. object.__setattr__(self, "lock_version", lock_version)
  502. object.__setattr__(self, "environments", environments)
  503. object.__setattr__(self, "requires_python", requires_python)
  504. object.__setattr__(self, "extras", extras)
  505. object.__setattr__(self, "dependency_groups", dependency_groups)
  506. object.__setattr__(self, "default_groups", default_groups)
  507. object.__setattr__(self, "created_by", created_by)
  508. object.__setattr__(self, "packages", packages)
  509. object.__setattr__(self, "tool", tool)
  510. @classmethod
  511. def _from_dict(cls, d: Mapping[str, Any]) -> Self:
  512. pylock = cls(
  513. lock_version=_get_required_as(d, str, Version, "lock-version"),
  514. environments=_get_sequence_as(d, str, Marker, "environments"),
  515. extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),
  516. dependency_groups=_get_sequence(d, str, "dependency-groups"),
  517. default_groups=_get_sequence(d, str, "default-groups"),
  518. created_by=_get_required(d, str, "created-by"),
  519. requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
  520. packages=_get_required_sequence_of_objects(d, Package, "packages"),
  521. tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
  522. )
  523. if not Version("1") <= pylock.lock_version < Version("2"):
  524. raise PylockUnsupportedVersionError(
  525. f"pylock version {pylock.lock_version} is not supported"
  526. )
  527. if pylock.lock_version > Version("1.0"):
  528. _logger.warning(
  529. "pylock minor version %s is not supported", pylock.lock_version
  530. )
  531. return pylock
  532. @classmethod
  533. def from_dict(cls, d: Mapping[str, Any], /) -> Self:
  534. """Create and validate a Pylock instance from a TOML dictionary.
  535. Raises :class:`PylockValidationError` if the input data is not
  536. spec-compliant.
  537. """
  538. return cls._from_dict(d)
  539. def to_dict(self) -> Mapping[str, Any]:
  540. """Convert the Pylock instance to a TOML dictionary."""
  541. return dataclasses.asdict(self, dict_factory=_toml_dict_factory)
  542. def validate(self) -> None:
  543. """Validate the Pylock instance against the specification.
  544. Raises :class:`PylockValidationError` otherwise."""
  545. self.from_dict(self.to_dict())