Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
_parser.py394 linesDownload Raw Back to packaging
1"""Handwritten parser of dependency specifiers.2 3The docstring for each __parse_* function contains EBNF-inspired grammar representing4the implementation.5"""6 7from __future__ import annotations8 9import ast10from typing import List, Literal, NamedTuple, Sequence, Tuple, Union11 12from ._tokenizer import DEFAULT_RULES, Tokenizer13 14 15class Node:16    __slots__ = ("value",)17 18    def __init__(self, value: str) -> None:19        self.value = value20 21    def __str__(self) -> str:22        return self.value23 24    def __repr__(self) -> str:25        return f"<{self.__class__.__name__}({self.value!r})>"26 27    def serialize(self) -> str:28        raise NotImplementedError29 30    def __getstate__(self) -> str:31        # Return just the value string for compactness and stability.32        return self.value33 34    def _restore_value(self, value: object) -> None:35        if not isinstance(value, str):36            raise TypeError(37                f"Cannot restore {self.__class__.__name__} value from {value!r}"38            )39        self.value = value40 41    def __setstate__(self, state: object) -> None:42        if isinstance(state, str):43            # New format (26.2+): just the value string.44            self._restore_value(state)45            return46        if isinstance(state, tuple) and len(state) == 2:47            # Old format (packaging <= 26.0, __slots__): (None, {slot: value}).48            _, slot_dict = state49            if isinstance(slot_dict, dict) and "value" in slot_dict:50                self._restore_value(slot_dict["value"])51                return52        if isinstance(state, dict) and "value" in state:53            # Old format (packaging <= 25.0, no __slots__): plain __dict__.54            self._restore_value(state["value"])55            return56        raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")57 58 59class Variable(Node):60    __slots__ = ()61 62    def serialize(self) -> str:63        return str(self)64 65 66class Value(Node):67    __slots__ = ()68 69    def serialize(self) -> str:70        return f'"{self}"'71 72 73class Op(Node):74    __slots__ = ()75 76    def serialize(self) -> str:77        return str(self)78 79 80MarkerLogical = Literal["and", "or"]81MarkerVar = Union[Variable, Value]82MarkerItem = Tuple[MarkerVar, Op, MarkerVar]83MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]84MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]]85 86 87class ParsedRequirement(NamedTuple):88    name: str89    url: str90    extras: list[str]91    specifier: str92    marker: MarkerList | None93 94 95# --------------------------------------------------------------------------------------96# Recursive descent parser for dependency specifier97# --------------------------------------------------------------------------------------98def parse_requirement(source: str) -> ParsedRequirement:99    return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES))100 101 102def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:103    """104    requirement = WS? IDENTIFIER WS? extras WS? requirement_details105    """106    tokenizer.consume("WS")107 108    name_token = tokenizer.expect(109        "IDENTIFIER", expected="package name at the start of dependency specifier"110    )111    name = name_token.text112    tokenizer.consume("WS")113 114    extras = _parse_extras(tokenizer)115    tokenizer.consume("WS")116 117    url, specifier, marker = _parse_requirement_details(tokenizer)118    tokenizer.expect("END", expected="end of dependency specifier")119 120    return ParsedRequirement(name, url, extras, specifier, marker)121 122 123def _parse_requirement_details(124    tokenizer: Tokenizer,125) -> tuple[str, str, MarkerList | None]:126    """127    requirement_details = AT URL (WS requirement_marker?)?128                        | specifier WS? (requirement_marker)?129    """130 131    specifier = ""132    url = ""133    marker = None134 135    if tokenizer.check("AT"):136        tokenizer.read()137        tokenizer.consume("WS")138 139        url_start = tokenizer.position140        url = tokenizer.expect("URL", expected="URL after @").text141        if tokenizer.check("END", peek=True):142            return (url, specifier, marker)143 144        tokenizer.expect("WS", expected="whitespace after URL")145 146        # The input might end after whitespace.147        if tokenizer.check("END", peek=True):148            return (url, specifier, marker)149 150        marker = _parse_requirement_marker(151            tokenizer,152            span_start=url_start,153            expected="semicolon (after URL and whitespace)",154        )155    else:156        specifier_start = tokenizer.position157        specifier = _parse_specifier(tokenizer)158        tokenizer.consume("WS")159 160        if tokenizer.check("END", peek=True):161            return (url, specifier, marker)162 163        marker = _parse_requirement_marker(164            tokenizer,165            span_start=specifier_start,166            expected=(167                "comma (within version specifier), semicolon (after version specifier)"168                if specifier169                else "semicolon (after name with no version specifier)"170            ),171        )172 173    return (url, specifier, marker)174 175 176def _parse_requirement_marker(177    tokenizer: Tokenizer, *, span_start: int, expected: str178) -> MarkerList:179    """180    requirement_marker = SEMICOLON marker WS?181    """182 183    if not tokenizer.check("SEMICOLON"):184        tokenizer.raise_syntax_error(185            f"Expected {expected} or end",186            span_start=span_start,187            span_end=None,188        )189    tokenizer.read()190 191    marker = _parse_marker(tokenizer)192    tokenizer.consume("WS")193 194    return marker195 196 197def _parse_extras(tokenizer: Tokenizer) -> list[str]:198    """199    extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?200    """201    if not tokenizer.check("LEFT_BRACKET", peek=True):202        return []203 204    with tokenizer.enclosing_tokens(205        "LEFT_BRACKET",206        "RIGHT_BRACKET",207        around="extras",208    ):209        tokenizer.consume("WS")210        extras = _parse_extras_list(tokenizer)211        tokenizer.consume("WS")212 213    return extras214 215 216def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:217    """218    extras_list = identifier (wsp* ',' wsp* identifier)*219    """220    extras: list[str] = []221 222    if not tokenizer.check("IDENTIFIER"):223        return extras224 225    extras.append(tokenizer.read().text)226 227    while True:228        tokenizer.consume("WS")229        if tokenizer.check("IDENTIFIER", peek=True):230            tokenizer.raise_syntax_error("Expected comma between extra names")231        elif not tokenizer.check("COMMA"):232            break233 234        tokenizer.read()235        tokenizer.consume("WS")236 237        extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma")238        extras.append(extra_token.text)239 240    return extras241 242 243def _parse_specifier(tokenizer: Tokenizer) -> str:244    """245    specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS246              | WS? version_many WS?247    """248    with tokenizer.enclosing_tokens(249        "LEFT_PARENTHESIS",250        "RIGHT_PARENTHESIS",251        around="version specifier",252    ):253        tokenizer.consume("WS")254        parsed_specifiers = _parse_version_many(tokenizer)255        tokenizer.consume("WS")256 257    return parsed_specifiers258 259 260def _parse_version_many(tokenizer: Tokenizer) -> str:261    """262    version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)?263    """264    parsed_specifiers = ""265    while tokenizer.check("SPECIFIER"):266        span_start = tokenizer.position267        parsed_specifiers += tokenizer.read().text268        if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):269            tokenizer.raise_syntax_error(270                ".* suffix can only be used with `==` or `!=` operators",271                span_start=span_start,272                span_end=tokenizer.position + 1,273            )274        if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):275            tokenizer.raise_syntax_error(276                "Local version label can only be used with `==` or `!=` operators",277                span_start=span_start,278                span_end=tokenizer.position,279            )280        tokenizer.consume("WS")281        if not tokenizer.check("COMMA"):282            break283        parsed_specifiers += tokenizer.read().text284        tokenizer.consume("WS")285 286    return parsed_specifiers287 288 289# --------------------------------------------------------------------------------------290# Recursive descent parser for marker expression291# --------------------------------------------------------------------------------------292def parse_marker(source: str) -> MarkerList:293    return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))294 295 296def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:297    retval = _parse_marker(tokenizer)298    tokenizer.expect("END", expected="end of marker expression")299    return retval300 301 302def _parse_marker(tokenizer: Tokenizer) -> MarkerList:303    """304    marker = marker_atom (BOOLOP marker_atom)+305    """306    expression = [_parse_marker_atom(tokenizer)]307    while tokenizer.check("BOOLOP"):308        token = tokenizer.read()309        expr_right = _parse_marker_atom(tokenizer)310        expression.extend((token.text, expr_right))311    return expression312 313 314def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:315    """316    marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?317                | WS? marker_item WS?318    """319 320    tokenizer.consume("WS")321    if tokenizer.check("LEFT_PARENTHESIS", peek=True):322        with tokenizer.enclosing_tokens(323            "LEFT_PARENTHESIS",324            "RIGHT_PARENTHESIS",325            around="marker expression",326        ):327            tokenizer.consume("WS")328            marker: MarkerAtom = _parse_marker(tokenizer)329            tokenizer.consume("WS")330    else:331        marker = _parse_marker_item(tokenizer)332    tokenizer.consume("WS")333    return marker334 335 336def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:337    """338    marker_item = WS? marker_var WS? marker_op WS? marker_var WS?339    """340    tokenizer.consume("WS")341    marker_var_left = _parse_marker_var(tokenizer)342    tokenizer.consume("WS")343    marker_op = _parse_marker_op(tokenizer)344    tokenizer.consume("WS")345    marker_var_right = _parse_marker_var(tokenizer)346    tokenizer.consume("WS")347    return (marker_var_left, marker_op, marker_var_right)348 349 350def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:  # noqa: RET503351    """352    marker_var = VARIABLE | QUOTED_STRING353    """354    if tokenizer.check("VARIABLE"):355        return process_env_var(tokenizer.read().text.replace(".", "_"))356    elif tokenizer.check("QUOTED_STRING"):357        return process_python_str(tokenizer.read().text)358    else:359        tokenizer.raise_syntax_error(360            message="Expected a marker variable or quoted string"361        )362 363 364def process_env_var(env_var: str) -> Variable:365    if env_var in ("platform_python_implementation", "python_implementation"):366        return Variable("platform_python_implementation")367    else:368        return Variable(env_var)369 370 371def process_python_str(python_str: str) -> Value:372    value = ast.literal_eval(python_str)373    return Value(str(value))374 375 376def _parse_marker_op(tokenizer: Tokenizer) -> Op:377    """378    marker_op = IN | NOT IN | OP379    """380    if tokenizer.check("IN"):381        tokenizer.read()382        return Op("in")383    elif tokenizer.check("NOT"):384        tokenizer.read()385        tokenizer.expect("WS", expected="whitespace after 'not'")386        tokenizer.expect("IN", expected="'in' after 'not'")387        return Op("not in")388    elif tokenizer.check("OP"):389        return Op(tokenizer.read().text)390    else:391        return tokenizer.raise_syntax_error(392            "Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in"393        )394 
codekingpro/portable-devtools · Team Ai