Source code for requirementslib.models.pipfile

# -*- coding: utf-8 -*-

from __future__ import absolute_import, print_function, unicode_literals

import copy
import itertools
import os
import sys

import attr
import plette.models.base
import plette.pipfiles
import tomlkit
from vistir.compat import FileNotFoundError, Path

from ..environment import MYPY_RUNNING
from ..exceptions import RequirementError
from ..utils import is_editable, is_vcs, merge_items
from .project import ProjectFile
from .requirements import Requirement
from .utils import get_url_name, optional_instance_of, tomlkit_value_to_python

if MYPY_RUNNING:
    from typing import Union, Any, Dict, Iterable, Mapping, List, Text

    package_type = Dict[Text, Dict[Text, Union[List[Text], Text]]]
    source_type = Dict[Text, Union[Text, bool]]
    sources_type = Iterable[source_type]
    meta_type = Dict[Text, Union[int, Dict[Text, Text], sources_type]]
    lockfile_type = Dict[Text, Union[package_type, meta_type]]


is_pipfile = optional_instance_of(plette.pipfiles.Pipfile)
is_path = optional_instance_of(Path)
is_projectfile = optional_instance_of(ProjectFile)


[docs]def reorder_source_keys(data): # type: (tomlkit.toml_document.TOMLDocument) -> tomlkit.toml_document.TOMLDocument sources = [] # type: sources_type for source_key in ["source", "sources"]: sources.extend(data.get(source_key, tomlkit.aot()).value) new_source_aot = tomlkit.aot() for entry in sources: table = tomlkit.table() # type: tomlkit.items.Table source_entry = PipfileLoader.populate_source(entry.copy()) for key in ["name", "url", "verify_ssl"]: table.update({key: source_entry[key]}) new_source_aot.append(table) data["source"] = new_source_aot if data.get("sources", None): del data["sources"] return data
[docs]class PipfileLoader(plette.pipfiles.Pipfile):
[docs] @classmethod def validate(cls, data): # type: (tomlkit.toml_document.TOMLDocument) -> None for key, klass in plette.pipfiles.PIPFILE_SECTIONS.items(): if key not in data or key == "sources": continue try: klass.validate(data[key]) except Exception: pass
[docs] @classmethod def ensure_package_sections(cls, data): # type: (tomlkit.toml_document.TOMLDocument[Text, Any]) -> tomlkit.toml_document.TOMLDocument[Text, Any] """ Ensure that all pipfile package sections are present in the given toml document :param :class:`~tomlkit.toml_document.TOMLDocument` data: The toml document to ensure package sections are present on :return: The updated toml document, ensuring ``packages`` and ``dev-packages`` sections are present :rtype: :class:`~tomlkit.toml_document.TOMLDocument` """ package_keys = ( k for k in plette.pipfiles.PIPFILE_SECTIONS.keys() if k.endswith("packages") ) for key in package_keys: if key not in data: data.update({key: tomlkit.table()}) return data
[docs] @classmethod def populate_source(cls, source): """Derive missing values of source from the existing fields.""" # Only URL pararemter is mandatory, let the KeyError be thrown. if "name" not in source: source["name"] = get_url_name(source["url"]) if "verify_ssl" not in source: source["verify_ssl"] = "https://" in source["url"] if not isinstance(source["verify_ssl"], bool): source["verify_ssl"] = str(source["verify_ssl"]).lower() == "true" return source
[docs] @classmethod def load(cls, f, encoding=None): # type: (Any, Text) -> PipfileLoader content = f.read() if encoding is not None: content = content.decode(encoding) _data = tomlkit.loads(content) should_reload = "source" not in _data _data = reorder_source_keys(_data) if should_reload: if "sources" in _data: content = tomlkit.dumps(_data) else: # 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 = plette.pipfiles.DEFAULT_SOURCE_TOML + sep + content data = tomlkit.loads(content) data = cls.ensure_package_sections(data) instance = cls(data) instance._data = dict(instance._data) return instance
def __contains__(self, key): # type: (Text) -> bool if key not in self._data: package_keys = self._data.get("packages", {}).keys() dev_package_keys = self._data.get("dev-packages", {}).keys() return any(key in pkg_list for pkg_list in (package_keys, dev_package_keys)) return True def __getattribute__(self, key): # type: (Text) -> Any if key == "source": return self._data[key] return super(PipfileLoader, self).__getattribute__(key)
[docs]@attr.s(slots=True) class Pipfile(object): path = attr.ib(validator=is_path, type=Path) projectfile = attr.ib(validator=is_projectfile, type=ProjectFile) _pipfile = attr.ib(type=PipfileLoader) _pyproject = attr.ib( default=attr.Factory(tomlkit.document), type=tomlkit.toml_document.TOMLDocument ) build_system = attr.ib(default=attr.Factory(dict), type=dict) _requirements = attr.ib(default=attr.Factory(list), type=list) _dev_requirements = attr.ib(default=attr.Factory(list), type=list) @path.default def _get_path(self): # type: () -> Path return Path(os.curdir).absolute() @projectfile.default def _get_projectfile(self): # type: () -> ProjectFile return self.load_projectfile(os.curdir, create=False) @_pipfile.default def _get_pipfile(self): # type: () -> Union[plette.pipfiles.Pipfile, PipfileLoader] return self.projectfile.model @property def root(self): return self.path.parent @property def extended_keys(self): return [ k for k in itertools.product( ("packages", "dev-packages"), ("", "vcs", "editable") ) ] @property def pipfile(self): # type: () -> Union[PipfileLoader, plette.pipfiles.Pipfile] return self._pipfile
[docs] def get_deps(self, dev=False, only=True): # type: (bool, bool) -> Dict[Text, Dict[Text, Union[List[Text], Text]]] deps = {} # type: Dict[Text, Dict[Text, Union[List[Text], Text]]] if dev: deps.update(dict(self.pipfile._data.get("dev-packages", {}))) if only: return deps return tomlkit_value_to_python( merge_items([deps, dict(self.pipfile._data.get("packages", {}))]) )
[docs] def get(self, k): # type: (Text) -> Any return self.__getitem__(k)
def __contains__(self, k): # type: (Text) -> bool check_pipfile = k in self.extended_keys or self.pipfile.__contains__(k) if check_pipfile: return True return False def __getitem__(self, k, *args, **kwargs): # type: ignore retval = None pipfile = self._pipfile section = None pkg_type = None try: retval = pipfile[k] except KeyError: if "-" in k: section, _, pkg_type = k.rpartition("-") vals = getattr(pipfile.get(section, {}), "_data", {}) vals = tomlkit_value_to_python(vals) if pkg_type == "vcs": retval = {k: v for k, v in vals.items() if is_vcs(v)} elif pkg_type == "editable": retval = {k: v for k, v in vals.items() if is_editable(v)} if retval is None: raise else: retval = getattr(retval, "_data", retval) return retval def __getattr__(self, k, *args, **kwargs): # type: ignore retval = None pipfile = super(Pipfile, self).__getattribute__("_pipfile") try: retval = super(Pipfile, self).__getattribute__(k) except AttributeError: retval = getattr(pipfile, k, None) if retval is not None: return retval return super(Pipfile, self).__getattribute__(k, *args, **kwargs) @property def requires_python(self): # type: () -> bool return getattr( self._pipfile.requires, "python_version", getattr(self._pipfile.requires, "python_full_version", None), ) @property def allow_prereleases(self): # type: () -> bool return self._pipfile.get("pipenv", {}).get("allow_prereleases", False)
[docs] @classmethod def read_projectfile(cls, path): # type: (Text) -> ProjectFile """Read the specified project file and provide an interface for writing/updating. :param Text path: Path to the target file. :return: A project file with the model and location for interaction :rtype: :class:`~requirementslib.models.project.ProjectFile` """ pf = ProjectFile.read(path, PipfileLoader, invalid_ok=True) return pf
[docs] @classmethod def load_projectfile(cls, path, create=False): # type: (Text, bool) -> ProjectFile """ Given a path, load or create the necessary pipfile. :param Text path: Path to the project root or pipfile :param bool create: Whether to create the pipfile if not found, defaults to True :raises OSError: Thrown if the project root directory doesn't exist :raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False`` :return: A project file instance for the supplied project :rtype: :class:`~requirementslib.models.project.ProjectFile` """ if not path: raise RuntimeError("Must pass a path to classmethod 'Pipfile.load'") if not isinstance(path, Path): path = Path(path).absolute() pipfile_path = path if path.is_file() else path.joinpath("Pipfile") project_path = pipfile_path.parent if not project_path.exists(): raise FileNotFoundError("%s is not a valid project path!" % path) elif not pipfile_path.exists() or not pipfile_path.is_file(): if not create: raise RequirementError("%s is not a valid Pipfile" % pipfile_path) return cls.read_projectfile(pipfile_path.as_posix())
[docs] @classmethod def load(cls, path, create=False): # type: (Text, bool) -> Pipfile """ Given a path, load or create the necessary pipfile. :param Text path: Path to the project root or pipfile :param bool create: Whether to create the pipfile if not found, defaults to True :raises OSError: Thrown if the project root directory doesn't exist :raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False`` :return: A pipfile instance pointing at the supplied project :rtype:: class:`~requirementslib.models.pipfile.Pipfile` """ projectfile = cls.load_projectfile(path, create=create) pipfile = projectfile.model creation_args = { "projectfile": projectfile, "pipfile": pipfile, "path": Path(projectfile.location), } return cls(**creation_args)
[docs] def write(self): # type: () -> None self.projectfile.model = copy.deepcopy(self._pipfile) self.projectfile.write()
@property def dev_packages(self): # type: () -> List[Requirement] return self.dev_requirements @property def packages(self): # type: () -> List[Requirement] return self.requirements @property def dev_requirements(self): # type: () -> List[Requirement] if not self._dev_requirements: packages = tomlkit_value_to_python(self.pipfile.get("dev-packages", {})) self._dev_requirements = [ Requirement.from_pipfile(k, v) for k, v in packages.items() if v is not None ] return self._dev_requirements @property def requirements(self): # type: () -> List[Requirement] if not self._requirements: packages = tomlkit_value_to_python(self.pipfile.get("packages", {})) self._requirements = [ Requirement.from_pipfile(k, v) for k, v in packages.items() if v is not None ] return self._requirements def _read_pyproject(self): # type: () -> None pyproject = self.path.parent.joinpath("pyproject.toml") if pyproject.exists(): self._pyproject = tomlkit.loads(pyproject.read_text()) build_system = self._pyproject.get("build-system", None) if build_system and not build_system.get("build_backend"): build_system["build-backend"] = "setuptools.build_meta:__legacy__" elif not build_system or not build_system.get("requires"): build_system = { "requires": ["setuptools>=40.8", "wheel"], "build-backend": "setuptools.build_meta:__legacy__", } self.build_system = build_system @property def build_requires(self): # type: () -> List[Text] if not self.build_system: self._read_pyproject() return self.build_system.get("requires", []) @property def build_backend(self): # type: () -> Text pyproject = self.path.parent.joinpath("pyproject.toml") if not self.build_system: self._read_pyproject() return self.build_system.get("build-backend", None)