import hashlib import json import pipenv.vendor.tomlkit as tomlkit from .models import ( DataModel, Hash, Requires, PipfileSection, Pipenv, PackageCollection, ScriptCollection, SourceCollection, ) PIPFILE_SECTIONS = { "source": SourceCollection, "packages": PackageCollection, "dev-packages": PackageCollection, "requires": Requires, "scripts": ScriptCollection, "pipfile": PipfileSection, "pipenv": Pipenv } DEFAULT_SOURCE_TOML = """\ [[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true """ class Pipfile(DataModel): """Representation of a Pipfile. """ __SCHEMA__ = {} @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: continue klass.validate(data[key]) package_categories = set(data.keys()) - set(PIPFILE_SECTIONS.keys()) for category in package_categories: PackageCollection.validate(data[category]) @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._data[key] try: return PIPFILE_SECTIONS[key](value) except KeyError: return value def __setitem__(self, key, value): if isinstance(value, DataModel): 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", {}), }, "default": self._data.get("packages", {}), "develop": self._data.get("dev-packages", {}), } for category, values in self._data.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)) def dump(self, f, encoding=None): content = tomlkit.dumps(self._data) 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