Skip to content

Commit

Permalink
add basic code
Browse files Browse the repository at this point in the history
  • Loading branch information
CagtayFabry committed Jan 30, 2024
1 parent d160bcb commit f167e37
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 20 deletions.
38 changes: 18 additions & 20 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/psf/black
rev: 22.1.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
- id: flake8
args: [--max-line-length=88]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-yaml
# ----- Python formatting -----
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.1.13
hooks:
# Run ruff linter.
- id: ruff
args:
- --quiet
- --fix
# Run ruff formatter.
- id: ruff-format
5 changes: 5 additions & 0 deletions pydeps2env/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""pydeps2env: helps to generate conda environment files from python package dependencies."""

from .environment import Environment

__all__ = ["Environment"]
111 changes: 111 additions & 0 deletions pydeps2env/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from dataclasses import dataclass, field
from packaging.requirements import Requirement
from pathlib import Path
from collections import defaultdict
import tomli as tomllib
import yaml
import warnings


def add_requirement(
req: Requirement | str,
requirements: dict[str, Requirement],
mode: str = "combine",
):
"""Add a requirement to existing requirement specification (in place)."""

if not isinstance(req, Requirement):
req = Requirement(req)

if req.name not in requirements:
requirements[req.name] = req
elif mode == "combine":
requirements[req.name].specifier &= req.specifier
elif mode == "replace":
requirements[req.name] = req
else:
raise ValueError(f"Unknown `mode` for add_requirement: {mode}")


def combine_requirements(
req1: dict[str, Requirement], req2: dict[str, Requirement]
) -> dict[str, Requirement]:
"""Combine multiple requirement listings."""
req1 = req1.copy()
for r in req2.values():
add_requirement(r, req1, mode="combine")

return req1


@dataclass
class Environment:
filename: str | Path
channels: list[str] = field(default_factory=lambda: ["conda-forge"])
pip_packages: set[str] = field(default_factory=set) # install via pip
requirements: dict[str, Requirement] = field(default_factory=dict, init=False)
build_system: dict[str, Requirement] = field(default_factory=dict, init=False)

def __post_init__(self):
if Path(self.filename).suffix == ".toml":
self.load_pyproject()

def load_pyproject(self):
with open(self.filename, "rb") as fh:
cp = defaultdict(dict, tomllib.load(fh))

if python := cp["project"].get("requires-python"):
add_requirement("python" + python, self.requirements)

for dep in cp.get("project").get("dependencies"):
add_requirement(dep, self.requirements)

for dep in cp.get("build-system").get("requires"):
add_requirement(dep, self.build_system)

def _get_dependencies(self, include_build_system: bool = True):
"""Get the default conda environment entries."""

reqs = self.requirements.copy()
if include_build_system:
reqs = combine_requirements(reqs, self.build_system)

_python = reqs.pop("python", None)

deps = [str(r) for r in reqs.values() if r.name not in self.pip_packages]
deps.sort(key=str.lower)
if _python:
deps = [str(_python)] + deps

pip = [str(r) for r in reqs.values() if r.name in self.pip_packages]
pip.sort(key=str.lower)

return deps, pip

def export(
self,
outfile: str | Path = "environment.yaml",
include_build_system: bool = True,
):
deps, pip = self._get_dependencies(include_build_system=include_build_system)

conda_env = {"channels": self.channels, "dependencies": deps.copy()}
if pip:
conda_env["dependencies"] += ["pip", {"pip": pip}]

if outfile is None:
return conda_env

p = Path(outfile)
if p.suffix in [".txt"]:
deps += pip
deps.sort(key=str.lower)
with open(p, "w") as outfile:
outfile.writelines("\n".join(deps))
else:
if p.suffix not in [".yaml", ".yml"]:
warnings.warn(
f"Unknown environment format `{p.suffix}`, generating conda yaml output."
)
with open(p, "w") as outfile:
yaml.dump(conda_env, outfile, default_flow_style=False)

0 comments on commit f167e37

Please sign in to comment.