Skip to content

Instantly share code, notes, and snippets.

@daskol
Last active September 29, 2023 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daskol/5513ff9c5b8a2d6b2a0e78f522dd2800 to your computer and use it in GitHub Desktop.
Save daskol/5513ff9c5b8a2d6b2a0e78f522dd2800 to your computer and use it in GitHub Desktop.
Py + Deps = <3
#!/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