Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
dependency_groups.py303 linesDownload Raw Back to packaging
1from __future__ import annotations2 3import re4from collections.abc import Mapping, Sequence5 6from .errors import _ErrorCollector7from .requirements import Requirement8 9__all__ = [10    "CyclicDependencyGroup",11    "DependencyGroupInclude",12    "DependencyGroupResolver",13    "DuplicateGroupNames",14    "InvalidDependencyGroupObject",15    "resolve_dependency_groups",16]17 18 19def __dir__() -> list[str]:20    return __all__21 22 23# -----------24# Error Types25# -----------26 27 28class DuplicateGroupNames(ValueError):29    """30    The same dependency groups were defined twice, with different non-normalized names.31    """32 33 34class CyclicDependencyGroup(ValueError):35    """36    The dependency group includes form a cycle.37    """38 39    def __init__(self, requested_group: str, group: str, include_group: str) -> None:40        self.requested_group = requested_group41        self.group = group42        self.include_group = include_group43 44        if include_group == group:45            reason = f"{group} includes itself"46        else:47            reason = f"{include_group} -> {group}, {group} -> {include_group}"48        super().__init__(49            "Cyclic dependency group include while resolving "50            f"{requested_group}: {reason}"51        )52 53 54# in the PEP 735 spec, the tables in dependency group lists were described as55# "Dependency Object Specifiers", but the only defined type of object was a56# "Dependency Group Include" -- hence the naming of this error as "Object"57class InvalidDependencyGroupObject(ValueError):58    """59    A member of a dependency group was identified as a dict, but was not in a valid60    format.61    """62 63 64# ------------------------65# Object Model & Interface66# ------------------------67 68 69class DependencyGroupInclude:70    __slots__ = ("include_group",)71 72    def __init__(self, include_group: str) -> None:73        """74        Initialize a DependencyGroupInclude.75 76        :param include_group: The name of the group referred to by this include.77        """78        self.include_group = include_group79 80    def __repr__(self) -> str:81        return f"{self.__class__.__name__}({self.include_group!r})"82 83 84class DependencyGroupResolver:85    """86    A resolver for Dependency Group data.87 88    This class handles caching, name normalization, cycle detection, and other89    parsing requirements. There are only two public methods for exploring the data:90    ``lookup()`` and ``resolve()``.91 92    :param dependency_groups: A mapping, as provided via pyproject93        ``[dependency-groups]``.94    """95 96    def __init__(97        self,98        dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],99    ) -> None:100        errors = _ErrorCollector()101 102        self.dependency_groups = _normalize_group_names(dependency_groups, errors)103 104        # a map of group names to parsed data105        self._parsed_groups: dict[106            str, tuple[Requirement | DependencyGroupInclude, ...]107        ] = {}108        # a map of group names to their ancestors, used for cycle detection109        self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}110        # a cache of completed resolutions to Requirement lists111        self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}112 113        errors.finalize("[dependency-groups] data was invalid")114 115    def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:116        """117        Lookup a group name, returning the parsed dependency data for that group.118        This will not resolve includes.119 120        :param group: the name of the group to lookup121        """122        group = _normalize_name(group)123 124        with _ErrorCollector().on_exit(125            f"[dependency-groups] data for {group!r} was malformed"126        ) as errors:127            return self._parse_group(group, errors)128 129    def resolve(self, group: str) -> tuple[Requirement, ...]:130        """131        Resolve a dependency group to a list of requirements.132 133        :param group: the name of the group to resolve134        """135        group = _normalize_name(group)136 137        with _ErrorCollector().on_exit(138            f"[dependency-groups] data for {group!r} was malformed"139        ) as errors:140            return self._resolve(group, group, errors)141 142    def _resolve(143        self, group: str, requested_group: str, errors: _ErrorCollector144    ) -> tuple[Requirement, ...]:145        """146        This is a helper for cached resolution to strings. It preserves the name of the147        group which the user initially requested in order to present a clearer error in148        the event that a cycle is detected.149 150        :param group: The normalized name of the group to resolve.151        :param requested_group: The group which was used in the original, user-facing152            request.153        """154        if group in self._resolve_cache:155            return self._resolve_cache[group]156 157        parsed = self._parse_group(group, errors)158 159        resolved_group = []160 161        for item in parsed:162            if isinstance(item, Requirement):163                resolved_group.append(item)164            elif isinstance(item, DependencyGroupInclude):165                include_group = _normalize_name(item.include_group)166 167                # if a group is cyclic, record the error168                # otherwise, follow the include_group reference169                #170                # this allows us to examine all includes in a group, even in the171                # presence of errors172                if include_group in self._include_graph_ancestors.get(group, ()):173                    errors.error(174                        CyclicDependencyGroup(175                            requested_group, group, item.include_group176                        )177                    )178                else:179                    self._include_graph_ancestors[include_group] = (180                        *self._include_graph_ancestors.get(group, ()),181                        group,182                    )183                    resolved_group.extend(184                        self._resolve(include_group, requested_group, errors)185                    )186            else:  # pragma: no cover187                raise NotImplementedError(188                    f"Invalid dependency group item after parse: {item}"189                )190 191        # in the event that errors were detected, present the group as empty and do not192        # cache the result193        # this ensures that repeated access to a cyclic group will raise multiple errors194        if errors.errors:195            return ()196 197        self._resolve_cache[group] = tuple(resolved_group)198        return self._resolve_cache[group]199 200    def _parse_group(201        self, group: str, errors: _ErrorCollector202    ) -> tuple[Requirement | DependencyGroupInclude, ...]:203        # short circuit -- never do the work twice204        if group in self._parsed_groups:205            return self._parsed_groups[group]206 207        if group not in self.dependency_groups:208            errors.error(LookupError(f"Dependency group '{group}' not found"))209            return ()210 211        raw_group = self.dependency_groups[group]212        if isinstance(raw_group, str):213            errors.error(214                TypeError(215                    f"Dependency group {group!r} contained a string rather than a list."216                )217            )218            return ()219 220        if not isinstance(raw_group, Sequence):221            errors.error(222                TypeError(f"Dependency group {group!r} is not a sequence type.")223            )224            return ()225 226        elements: list[Requirement | DependencyGroupInclude] = []227        for item in raw_group:228            if isinstance(item, str):229                # packaging.requirements.Requirement parsing ensures that this is a230                # valid PEP 508 Dependency Specifier231                # raises InvalidRequirement on failure232                elements.append(Requirement(item))233            elif isinstance(item, Mapping):234                if tuple(item.keys()) != ("include-group",):235                    errors.error(236                        InvalidDependencyGroupObject(237                            f"Invalid dependency group item: {item!r}"238                        )239                    )240                else:241                    include_group = item["include-group"]242                    elements.append(DependencyGroupInclude(include_group=include_group))243            else:244                errors.error(TypeError(f"Invalid dependency group item: {item!r}"))245 246        self._parsed_groups[group] = tuple(elements)247        return self._parsed_groups[group]248 249 250# --------------------251# Functional Interface252# --------------------253 254 255def resolve_dependency_groups(256    dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str257) -> tuple[str, ...]:258    """259    Resolve a dependency group to a tuple of requirements, as strings.260 261    :param dependency_groups: the parsed contents of the ``[dependency-groups]`` table262        from ``pyproject.toml``263    :param groups: the name of the group(s) to resolve264    """265    resolver = DependencyGroupResolver(dependency_groups)266    return tuple(str(r) for group in groups for r in resolver.resolve(group))267 268 269# ----------------270# internal helpers271# ----------------272 273 274_NORMALIZE_PATTERN = re.compile(r"[-_.]+")275 276 277def _normalize_name(name: str) -> str:278    return _NORMALIZE_PATTERN.sub("-", name).lower()279 280 281def _normalize_group_names(282    dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],283    errors: _ErrorCollector,284) -> dict[str, Sequence[str | Mapping[str, str]]]:285    original_names: dict[str, list[str]] = {}286    normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}287 288    for group_name, value in dependency_groups.items():289        normed_group_name = _normalize_name(group_name)290        original_names.setdefault(normed_group_name, []).append(group_name)291        normalized_groups[normed_group_name] = value292 293    for normed_name, names in original_names.items():294        if len(names) > 1:295            errors.error(296                DuplicateGroupNames(297                    "Duplicate dependency group names: "298                    f"{normed_name} ({', '.join(names)})"299                )300            )301 302    return normalized_groups303 
codekingpro/portable-devtools · Team Ai