xref: /aosp_15_r20/external/bazelbuild-rules_python/python/private/pypi/parse_requirements_txt.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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