Last active
September 29, 2023 15:35
-
-
Save daskol/5513ff9c5b8a2d6b2a0e78f522dd2800 to your computer and use it in GitHub Desktop.
Py + Deps = <3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
"""Py + Deps = <3: Simple script to install only dependencies of a python | |
package which follows PEP-517/PEP-518 guidelines. | |
""" | |
from argparse import ArgumentParser, Namespace | |
from dataclasses import dataclass, field | |
from pathlib import Path | |
from subprocess import check_call | |
from sys import version_info | |
from tempfile import TemporaryDirectory | |
from typing import Dict, List | |
if version_info < (3, 10): | |
from tomli import load | |
else: | |
from tomllib import load | |
parser = ArgumentParser(description=__doc__) | |
parser.add_argument('-i', | |
'--install', | |
action='store_true', | |
help='install dependencies') | |
parser.add_argument('-e', | |
'--extras', | |
default=[], | |
type=str, | |
action='append', | |
help='list of optional dependencies to install in advance') | |
parser.add_argument('--extra-index-url', | |
default=None, | |
type=str, | |
help='extra PyPI index') | |
parser.add_argument('package_dir', | |
default=Path(), | |
type=Path, | |
help='path to package root') | |
@dataclass | |
class Dependencies: | |
required: List[str] | |
optional: Dict[str, List[str]] = field(default=dict) | |
@staticmethod | |
def from_package_dir(path: Path) -> 'Dependencies': | |
if not path.is_dir(): | |
raise ValueError(f'Expected directory at {path}.') | |
with open(path / 'pyproject.toml', 'rb') as fin: | |
config = load(fin) | |
project = config.get('project', {}) | |
required_deps = project.get('dependencies', []) | |
optional_deps = project.get('optional-dependencies', {}) | |
return Dependencies(required_deps, optional_deps) | |
def install(self, extras: List[str], extra_opts: List[str] = []): | |
with TemporaryDirectory() as tmp_dir: | |
path = Path(tmp_dir) / 'requirements.txt' | |
self.to_file(path, extras) | |
check_call(['pip', 'install', '-r', str(path), *extra_opts]) | |
def to_file(self, path: Path, extras: List[str] = []): | |
def write_deps(fout, deps): | |
for dep in deps: | |
fout.write(dep) | |
fout.write('\n') | |
with open(path, 'w') as fout: | |
write_deps(fout, self.required) | |
for extra in extras: | |
if (deps := self.optional.get(extra)) is None: | |
raise KeyError('There is no such extra dependencies ' | |
f'with key {extra}.') | |
write_deps(fout, deps) | |
def format_dependencies(deps: List[str]) -> str: | |
if len(deps) == 0: | |
return 'n/a' | |
return ', '.join(deps) | |
def main(args: Namespace): | |
deps = Dependencies.from_package_dir(args.package_dir) | |
if args.install: | |
extra_opts = [] | |
if args.extra_index_url is not None: | |
extra_opts += ['--extra-index-url', args.extra_index_url] | |
deps.install(args.extras, extra_opts) | |
else: | |
print('Required:', format_dependencies(deps.required)) | |
for key, val in deps.optional.items(): | |
print(f'Optional[{key}]:', format_dependencies(val)) | |
if __name__ == '__main__': | |
main(parser.parse_args()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment