|
7 | 7 | def load_requirements(*requirements_paths): |
8 | 8 | """ |
9 | 9 | Load all requirements from the specified requirements files. |
| 10 | +
|
| 11 | + Requirements will include any constraints from files specified |
| 12 | + with -c in the requirements files. |
10 | 13 | Returns a list of requirement strings. |
11 | 14 | """ |
12 | | - requirements = set() |
| 15 | + # UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why. |
| 16 | + |
| 17 | + # e.g. {"django": "Django", "confluent-kafka": "confluent_kafka[avro]"} |
| 18 | + by_canonical_name = {} |
| 19 | + |
| 20 | + def check_name_consistent(package): |
| 21 | + """ |
| 22 | + Raise exception if package is named different ways. |
| 23 | +
|
| 24 | + This ensures that packages are named consistently so we can match |
| 25 | + constraints to packages. It also ensures that if we require a package |
| 26 | + with extras we don't constrain it without mentioning the extras (since |
| 27 | + that too would interfere with matching constraints.) |
| 28 | + """ |
| 29 | + canonical = package.lower().replace('_', '-').split('[')[0] |
| 30 | + seen_spelling = by_canonical_name.get(canonical) |
| 31 | + if seen_spelling is None: |
| 32 | + by_canonical_name[canonical] = package |
| 33 | + elif seen_spelling != package: |
| 34 | + raise Exception( |
| 35 | + f'Encountered both "{seen_spelling}" and "{package}" in requirements ' |
| 36 | + 'and constraints files; please use just one or the other.' |
| 37 | + ) |
| 38 | + |
| 39 | + requirements = {} |
| 40 | + constraint_files = set() |
| 41 | + |
| 42 | + # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...") |
| 43 | + re_package_name_base_chars = r"a-zA-Z0-9\-_." # chars allowed in base package name |
| 44 | + # Two groups: name[maybe,extras], and optionally a constraint |
| 45 | + requirement_line_regex = re.compile( |
| 46 | + r"([%s]+(?:\[[%s,\s]+\])?)([<>=][^#\s]+)?" |
| 47 | + % (re_package_name_base_chars, re_package_name_base_chars) |
| 48 | + ) |
| 49 | + |
| 50 | + def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present): |
| 51 | + regex_match = requirement_line_regex.match(current_line) |
| 52 | + if regex_match: |
| 53 | + package = regex_match.group(1) |
| 54 | + version_constraints = regex_match.group(2) |
| 55 | + check_name_consistent(package) |
| 56 | + existing_version_constraints = current_requirements.get(package, None) |
| 57 | + # It's fine to add constraints to an unconstrained package, |
| 58 | + # but raise an error if there are already constraints in place. |
| 59 | + if existing_version_constraints and existing_version_constraints != version_constraints: |
| 60 | + raise BaseException(f'Multiple constraint definitions found for {package}:' |
| 61 | + f' "{existing_version_constraints}" and "{version_constraints}".' |
| 62 | + f'Combine constraints into one location with {package}' |
| 63 | + f'{existing_version_constraints},{version_constraints}.') |
| 64 | + if add_if_not_present or package in current_requirements: |
| 65 | + current_requirements[package] = version_constraints |
| 66 | + |
| 67 | + # Read requirements from .in files and store the path to any |
| 68 | + # constraint files that are pulled in. |
13 | 69 | for path in requirements_paths: |
14 | 70 | with open(path) as reqs: |
15 | | - requirements.update( |
16 | | - line.split('#')[0].strip() for line in reqs |
17 | | - if is_requirement(line.strip()) |
18 | | - ) |
19 | | - return list(requirements) |
| 71 | + for line in reqs: |
| 72 | + if is_requirement(line): |
| 73 | + add_version_constraint_or_raise(line, requirements, True) |
| 74 | + if line and line.startswith('-c') and not line.startswith('-c http'): |
| 75 | + constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip()) |
| 76 | + |
| 77 | + # process constraint files: add constraints to existing requirements |
| 78 | + for constraint_file in constraint_files: |
| 79 | + with open(constraint_file) as reader: |
| 80 | + for line in reader: |
| 81 | + if is_requirement(line): |
| 82 | + add_version_constraint_or_raise(line, requirements, False) |
| 83 | + |
| 84 | + # process back into list of pkg><=constraints strings |
| 85 | + constrained_requirements = [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())] |
| 86 | + return constrained_requirements |
20 | 87 |
|
21 | 88 |
|
22 | 89 | def is_requirement(line): |
23 | 90 | """ |
24 | | - Return True if the requirement line is a package requirement; |
25 | | - that is, it is not blank, a comment, a URL, or an included file. |
| 91 | + Return True if the requirement line is a package requirement. |
| 92 | +
|
| 93 | + Returns: |
| 94 | + bool: True if the line is not blank, a comment, |
| 95 | + a URL, or an included file |
26 | 96 | """ |
27 | | - return line and not line.startswith(('-r', '#', '-e', 'git+', '-c')) |
| 97 | + # UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why |
| 98 | + |
| 99 | + return line and line.strip() and not line.startswith(('-r', '#', '-e', 'git+', '-c')) |
28 | 100 |
|
29 | 101 |
|
30 | 102 | def get_version(*file_paths): |
|
0 commit comments