| 48 | |
| 49 | |
| 50 | def apply_script_metadata( |
| 51 | scripts, # type: Sequence[str] |
| 52 | requirement_configuration=None, # type: Optional[RequirementConfiguration] |
| 53 | target_configuration=None, # type: Optional[TargetConfiguration] |
| 54 | ): |
| 55 | # type: (...) -> ScriptMetadataApplication |
| 56 | |
| 57 | script_metadatas = [] # type: List[ScriptMetadata] |
| 58 | requirements = OrderedSet( |
| 59 | requirement_configuration.requirements |
| 60 | if requirement_configuration and requirement_configuration.requirements |
| 61 | else () |
| 62 | ) # type: OrderedSet[ParsedRequirement] |
| 63 | requires_python = SpecifierSet() |
| 64 | for script in scripts: |
| 65 | with open(script, "rb") as fp: |
| 66 | # N.B.: The default encoding for python source files prior to Python 3.15 is ascii, |
| 67 | # but since utf-8 is a superset and people might forget to use encoding comment lines, |
| 68 | # this is a strictly better default to use. In fact, PEP-723 mandates this default. |
| 69 | # |
| 70 | # See: |
| 71 | # * https://packaging.python.org/en/latest/specifications/inline-script-metadata/ |
| 72 | # * https://peps.python.org/pep-0263/ |
| 73 | # * https://peps.python.org/pep-0686/ |
| 74 | encoding = "utf-8" # type: Text |
| 75 | lines_read = 0 |
| 76 | while lines_read < 2: |
| 77 | line = fp.readline() |
| 78 | lines_read += 1 |
| 79 | match = re.search( |
| 80 | br"^[ \t\f]*#.*?coding[:=][ \t]*(?P<encoding>[-_.a-zA-Z0-9]+)", line |
| 81 | ) |
| 82 | if match: |
| 83 | encoding = match.group("encoding").decode("ascii") |
| 84 | break |
| 85 | |
| 86 | fp.seek(0, os.SEEK_SET) |
| 87 | script_metadata = ScriptMetadata.parse(fp.read().decode(encoding), source=fp.name) |
| 88 | script_metadatas.append(script_metadata) |
| 89 | requirements.update(script_metadata.dependencies) |
| 90 | requires_python &= script_metadata.requires_python |
| 91 | |
| 92 | if isinstance(specifier_sets.as_range(requires_python), UnsatisfiableSpecifierSet): |
| 93 | raise Unsatisfiable( |
| 94 | dedent( |
| 95 | """\ |
| 96 | The requires-python metadata of two or more of the specified scripts is in conflict. |
| 97 | Given the scripts: {scripts} and {script} |
| 98 | The resulting Python requirement is '{requires_python}' which is not satisfiable. |
| 99 | """ |
| 100 | ).format( |
| 101 | scripts=", ".join(scripts[:-1]), script=scripts[-1], requires_python=requires_python |
| 102 | ) |
| 103 | ) |
| 104 | |
| 105 | if target_configuration and requires_python: |
| 106 | interpreter_constraints = ( |
| 107 | target_configuration.interpreter_configuration.interpreter_constraints.constraints |