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