1# Copyright 2023 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Pip requirements parser for Starlark.""" 16 17_STATE = struct( 18 # Consume extraneous whitespace 19 ConsumeSpace = 0, 20 # Consume a comment 21 ConsumeComment = 1, 22 # Parse the name of a pip package 23 ParseDependency = 2, 24 # Parse a full requirement line 25 ParseRequirement = 3, 26 # Parse a pip option 27 ParseOption = 4, 28) 29 30EOF = {} 31 32def parse_requirements_txt(content): 33 """A simplistic (and incomplete) pip requirements lockfile parser. 34 35 Parses package names and their full requirement lines, as well pip 36 options. 37 38 Args: 39 content: lockfile content as a string 40 41 Returns: 42 Struct with fields `requirements` and `options`. 43 44 requirements: List of requirements, where each requirement is a 2-element 45 tuple containing the package name and the requirement line. 46 E.g., [(certifi', 'certifi==2021.10.8 --hash=sha256:7888...'), ...] 47 48 options: List of pip option lines 49 """ 50 content = content.replace("\r", "") 51 52 result = struct( 53 requirements = [], 54 options = [], 55 ) 56 state = _STATE.ConsumeSpace 57 buffer = "" 58 59 inputs = content.elems()[:] 60 inputs.append(EOF) 61 62 for input in inputs: 63 if state == _STATE.ConsumeSpace: 64 (state, buffer) = _handleConsumeSpace(input) 65 elif state == _STATE.ConsumeComment: 66 (state, buffer) = _handleConsumeComment(input, buffer, result) 67 elif state == _STATE.ParseDependency: 68 (state, buffer) = _handleParseDependency(input, buffer, result) 69 elif state == _STATE.ParseOption: 70 (state, buffer) = _handleParseOption(input, buffer, result) 71 elif state == _STATE.ParseRequirement: 72 (state, buffer) = _handleParseRequirement(input, buffer, result) 73 else: 74 fail("Unknown state %d" % state) 75 76 return result 77 78def _handleConsumeSpace(input): 79 if input == EOF: 80 return (_STATE.ConsumeSpace, "") 81 if input.isspace(): 82 return (_STATE.ConsumeSpace, "") 83 elif input == "#": 84 return (_STATE.ConsumeComment, "") 85 elif input == "-": 86 return (_STATE.ParseOption, input) 87 88 return (_STATE.ParseDependency, input) 89 90def _handleConsumeComment(input, buffer, result): 91 if input == "\n": 92 if len(result.requirements) > 0 and len(result.requirements[-1]) == 1: 93 result.requirements[-1] = (result.requirements[-1][0], buffer.rstrip(" \n")) 94 return (_STATE.ConsumeSpace, "") 95 elif len(buffer) > 0: 96 result.options.append(buffer.rstrip(" \n")) 97 return (_STATE.ConsumeSpace, "") 98 return (_STATE.ConsumeSpace, "") 99 return (_STATE.ConsumeComment, buffer) 100 101def _handleParseDependency(input, buffer, result): 102 if input == EOF: 103 fail("Enountered unexpected end of file while parsing requirement") 104 elif input.isspace() or input in [">", "<", "~", "=", ";", "["]: 105 result.requirements.append((buffer,)) 106 return (_STATE.ParseRequirement, buffer + input) 107 108 return (_STATE.ParseDependency, buffer + input) 109 110def _handleParseOption(input, buffer, result): 111 if input == "\n" and buffer.endswith("\\"): 112 return (_STATE.ParseOption, buffer[0:-1]) 113 elif input == " ": 114 result.options.append(buffer.rstrip("\n")) 115 return (_STATE.ParseOption, "") 116 elif input == "\n" or input == EOF: 117 result.options.append(buffer.rstrip("\n")) 118 return (_STATE.ConsumeSpace, "") 119 elif input == "#" and (len(buffer) == 0 or buffer[-1].isspace()): 120 return (_STATE.ConsumeComment, buffer) 121 122 return (_STATE.ParseOption, buffer + input) 123 124def _handleParseRequirement(input, buffer, result): 125 if input == "\n" and buffer.endswith("\\"): 126 return (_STATE.ParseRequirement, buffer[0:-1]) 127 elif input == "\n" or input == EOF: 128 result.requirements[-1] = (result.requirements[-1][0], buffer.rstrip(" \n")) 129 return (_STATE.ConsumeSpace, "") 130 elif input == "#" and (len(buffer) == 0 or buffer[-1].isspace()): 131 return (_STATE.ConsumeComment, buffer) 132 133 return (_STATE.ParseRequirement, buffer + input) 134