Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_parser.py357 linesDownload Raw Back to packaging
1"""Handwritten parser of dependency specifiers.2 3The docstring for each __parse_* function contains ENBF-inspired grammar representing4the implementation.5"""6 7import ast8from typing import Any, List, NamedTuple, Optional, Tuple, Union9 10from ._tokenizer import DEFAULT_RULES, Tokenizer11 12 13class Node:14    def __init__(self, value: str) -> None:15        self.value = value16 17    def __str__(self) -> str:18        return self.value19 20    def __repr__(self) -> str:21        return f"<{self.__class__.__name__}('{self}')>"22 23    def serialize(self) -> str:24        raise NotImplementedError25 26 27class Variable(Node):28    def serialize(self) -> str:29        return str(self)30 31 32class Value(Node):33    def serialize(self) -> str:34        return f'"{self}"'35 36 37class Op(Node):38    def serialize(self) -> str:39        return str(self)40 41 42MarkerVar = Union[Variable, Value]43MarkerItem = Tuple[MarkerVar, Op, MarkerVar]44# MarkerAtom = Union[MarkerItem, List["MarkerAtom"]]45# MarkerList = List[Union["MarkerList", MarkerAtom, str]]46# mypy does not support recursive type definition47# https://github.com/python/mypy/issues/73148MarkerAtom = Any49MarkerList = List[Any]50 51 52class ParsedRequirement(NamedTuple):53    name: str54    url: str55    extras: List[str]56    specifier: str57    marker: Optional[MarkerList]58 59 60# --------------------------------------------------------------------------------------61# Recursive descent parser for dependency specifier62# --------------------------------------------------------------------------------------63def parse_requirement(source: str) -> ParsedRequirement:64    return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES))65 66 67def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:68    """69    requirement = WS? IDENTIFIER WS? extras WS? requirement_details70    """71    tokenizer.consume("WS")72 73    name_token = tokenizer.expect(74        "IDENTIFIER", expected="package name at the start of dependency specifier"75    )76    name = name_token.text77    tokenizer.consume("WS")78 79    extras = _parse_extras(tokenizer)80    tokenizer.consume("WS")81 82    url, specifier, marker = _parse_requirement_details(tokenizer)83    tokenizer.expect("END", expected="end of dependency specifier")84 85    return ParsedRequirement(name, url, extras, specifier, marker)86 87 88def _parse_requirement_details(89    tokenizer: Tokenizer,90) -> Tuple[str, str, Optional[MarkerList]]:91    """92    requirement_details = AT URL (WS requirement_marker?)?93                        | specifier WS? (requirement_marker)?94    """95 96    specifier = ""97    url = ""98    marker = None99 100    if tokenizer.check("AT"):101        tokenizer.read()102        tokenizer.consume("WS")103 104        url_start = tokenizer.position105        url = tokenizer.expect("URL", expected="URL after @").text106        if tokenizer.check("END", peek=True):107            return (url, specifier, marker)108 109        tokenizer.expect("WS", expected="whitespace after URL")110 111        # The input might end after whitespace.112        if tokenizer.check("END", peek=True):113            return (url, specifier, marker)114 115        marker = _parse_requirement_marker(116            tokenizer, span_start=url_start, after="URL and whitespace"117        )118    else:119        specifier_start = tokenizer.position120        specifier = _parse_specifier(tokenizer)121        tokenizer.consume("WS")122 123        if tokenizer.check("END", peek=True):124            return (url, specifier, marker)125 126        marker = _parse_requirement_marker(127            tokenizer,128            span_start=specifier_start,129            after=(130                "version specifier"131                if specifier132                else "name and no valid version specifier"133            ),134        )135 136    return (url, specifier, marker)137 138 139def _parse_requirement_marker(140    tokenizer: Tokenizer, *, span_start: int, after: str141) -> MarkerList:142    """143    requirement_marker = SEMICOLON marker WS?144    """145 146    if not tokenizer.check("SEMICOLON"):147        tokenizer.raise_syntax_error(148            f"Expected end or semicolon (after {after})",149            span_start=span_start,150        )151    tokenizer.read()152 153    marker = _parse_marker(tokenizer)154    tokenizer.consume("WS")155 156    return marker157 158 159def _parse_extras(tokenizer: Tokenizer) -> List[str]:160    """161    extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?162    """163    if not tokenizer.check("LEFT_BRACKET", peek=True):164        return []165 166    with tokenizer.enclosing_tokens(167        "LEFT_BRACKET",168        "RIGHT_BRACKET",169        around="extras",170    ):171        tokenizer.consume("WS")172        extras = _parse_extras_list(tokenizer)173        tokenizer.consume("WS")174 175    return extras176 177 178def _parse_extras_list(tokenizer: Tokenizer) -> List[str]:179    """180    extras_list = identifier (wsp* ',' wsp* identifier)*181    """182    extras: List[str] = []183 184    if not tokenizer.check("IDENTIFIER"):185        return extras186 187    extras.append(tokenizer.read().text)188 189    while True:190        tokenizer.consume("WS")191        if tokenizer.check("IDENTIFIER", peek=True):192            tokenizer.raise_syntax_error("Expected comma between extra names")193        elif not tokenizer.check("COMMA"):194            break195 196        tokenizer.read()197        tokenizer.consume("WS")198 199        extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma")200        extras.append(extra_token.text)201 202    return extras203 204 205def _parse_specifier(tokenizer: Tokenizer) -> str:206    """207    specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS208              | WS? version_many WS?209    """210    with tokenizer.enclosing_tokens(211        "LEFT_PARENTHESIS",212        "RIGHT_PARENTHESIS",213        around="version specifier",214    ):215        tokenizer.consume("WS")216        parsed_specifiers = _parse_version_many(tokenizer)217        tokenizer.consume("WS")218 219    return parsed_specifiers220 221 222def _parse_version_many(tokenizer: Tokenizer) -> str:223    """224    version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)?225    """226    parsed_specifiers = ""227    while tokenizer.check("SPECIFIER"):228        span_start = tokenizer.position229        parsed_specifiers += tokenizer.read().text230        if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):231            tokenizer.raise_syntax_error(232                ".* suffix can only be used with `==` or `!=` operators",233                span_start=span_start,234                span_end=tokenizer.position + 1,235            )236        if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):237            tokenizer.raise_syntax_error(238                "Local version label can only be used with `==` or `!=` operators",239                span_start=span_start,240                span_end=tokenizer.position,241            )242        tokenizer.consume("WS")243        if not tokenizer.check("COMMA"):244            break245        parsed_specifiers += tokenizer.read().text246        tokenizer.consume("WS")247 248    return parsed_specifiers249 250 251# --------------------------------------------------------------------------------------252# Recursive descent parser for marker expression253# --------------------------------------------------------------------------------------254def parse_marker(source: str) -> MarkerList:255    return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))256 257 258def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:259    retval = _parse_marker(tokenizer)260    tokenizer.expect("END", expected="end of marker expression")261    return retval262 263 264def _parse_marker(tokenizer: Tokenizer) -> MarkerList:265    """266    marker = marker_atom (BOOLOP marker_atom)+267    """268    expression = [_parse_marker_atom(tokenizer)]269    while tokenizer.check("BOOLOP"):270        token = tokenizer.read()271        expr_right = _parse_marker_atom(tokenizer)272        expression.extend((token.text, expr_right))273    return expression274 275 276def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:277    """278    marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?279                | WS? marker_item WS?280    """281 282    tokenizer.consume("WS")283    if tokenizer.check("LEFT_PARENTHESIS", peek=True):284        with tokenizer.enclosing_tokens(285            "LEFT_PARENTHESIS",286            "RIGHT_PARENTHESIS",287            around="marker expression",288        ):289            tokenizer.consume("WS")290            marker: MarkerAtom = _parse_marker(tokenizer)291            tokenizer.consume("WS")292    else:293        marker = _parse_marker_item(tokenizer)294    tokenizer.consume("WS")295    return marker296 297 298def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:299    """300    marker_item = WS? marker_var WS? marker_op WS? marker_var WS?301    """302    tokenizer.consume("WS")303    marker_var_left = _parse_marker_var(tokenizer)304    tokenizer.consume("WS")305    marker_op = _parse_marker_op(tokenizer)306    tokenizer.consume("WS")307    marker_var_right = _parse_marker_var(tokenizer)308    tokenizer.consume("WS")309    return (marker_var_left, marker_op, marker_var_right)310 311 312def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:313    """314    marker_var = VARIABLE | QUOTED_STRING315    """316    if tokenizer.check("VARIABLE"):317        return process_env_var(tokenizer.read().text.replace(".", "_"))318    elif tokenizer.check("QUOTED_STRING"):319        return process_python_str(tokenizer.read().text)320    else:321        tokenizer.raise_syntax_error(322            message="Expected a marker variable or quoted string"323        )324 325 326def process_env_var(env_var: str) -> Variable:327    if env_var in ("platform_python_implementation", "python_implementation"):328        return Variable("platform_python_implementation")329    else:330        return Variable(env_var)331 332 333def process_python_str(python_str: str) -> Value:334    value = ast.literal_eval(python_str)335    return Value(str(value))336 337 338def _parse_marker_op(tokenizer: Tokenizer) -> Op:339    """340    marker_op = IN | NOT IN | OP341    """342    if tokenizer.check("IN"):343        tokenizer.read()344        return Op("in")345    elif tokenizer.check("NOT"):346        tokenizer.read()347        tokenizer.expect("WS", expected="whitespace after 'not'")348        tokenizer.expect("IN", expected="'in' after 'not'")349        return Op("not in")350    elif tokenizer.check("OP"):351        return Op(tokenizer.read().text)352    else:353        return tokenizer.raise_syntax_error(354            "Expected marker operator, one of "355            "<=, <, !=, ==, >=, >, ~=, ===, in, not in"356        )357 
codekingpro/portable-devtools · Team Ai