Skip to content

Commit f167e37

Browse files
committed
add basic code
1 parent d160bcb commit f167e37

File tree

3 files changed

+134
-20
lines changed

3 files changed

+134
-20
lines changed

.pre-commit-config.yaml

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
repos:
2-
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.1.0
4-
hooks:
5-
- id: trailing-whitespace
6-
args: [--markdown-linebreak-ext=md]
7-
- id: end-of-file-fixer
8-
- id: check-yaml
9-
- repo: https://github.com/psf/black
10-
rev: 22.1.0
11-
hooks:
12-
- id: black
13-
- repo: https://github.com/pycqa/isort
14-
rev: 5.10.1
15-
hooks:
16-
- id: isort
17-
- repo: https://github.com/PyCQA/flake8
18-
rev: 4.0.1
19-
hooks:
20-
- id: flake8
21-
args: [--max-line-length=88]
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.1.0
4+
hooks:
5+
- id: trailing-whitespace
6+
args: [--markdown-linebreak-ext=md]
7+
- id: end-of-file-fixer
8+
- id: check-yaml
9+
# ----- Python formatting -----
10+
- repo: https://github.com/charliermarsh/ruff-pre-commit
11+
rev: v0.1.13
12+
hooks:
13+
# Run ruff linter.
14+
- id: ruff
15+
args:
16+
- --quiet
17+
- --fix
18+
# Run ruff formatter.
19+
- id: ruff-format

pydeps2env/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""pydeps2env: helps to generate conda environment files from python package dependencies."""
2+
3+
from .environment import Environment
4+
5+
__all__ = ["Environment"]

pydeps2env/environment.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from dataclasses import dataclass, field
2+
from packaging.requirements import Requirement
3+
from pathlib import Path
4+
from collections import defaultdict
5+
import tomli as tomllib
6+
import yaml
7+
import warnings
8+
9+
10+
def add_requirement(
11+
req: Requirement | str,
12+
requirements: dict[str, Requirement],
13+
mode: str = "combine",
14+
):
15+
"""Add a requirement to existing requirement specification (in place)."""
16+
17+
if not isinstance(req, Requirement):
18+
req = Requirement(req)
19+
20+
if req.name not in requirements:
21+
requirements[req.name] = req
22+
elif mode == "combine":
23+
requirements[req.name].specifier &= req.specifier
24+
elif mode == "replace":
25+
requirements[req.name] = req
26+
else:
27+
raise ValueError(f"Unknown `mode` for add_requirement: {mode}")
28+
29+
30+
def combine_requirements(
31+
req1: dict[str, Requirement], req2: dict[str, Requirement]
32+
) -> dict[str, Requirement]:
33+
"""Combine multiple requirement listings."""
34+
req1 = req1.copy()
35+
for r in req2.values():
36+
add_requirement(r, req1, mode="combine")
37+
38+
return req1
39+
40+
41+
@dataclass
42+
class Environment:
43+
filename: str | Path
44+
channels: list[str] = field(default_factory=lambda: ["conda-forge"])
45+
pip_packages: set[str] = field(default_factory=set) # install via pip
46+
requirements: dict[str, Requirement] = field(default_factory=dict, init=False)
47+
build_system: dict[str, Requirement] = field(default_factory=dict, init=False)
48+
49+
def __post_init__(self):
50+
if Path(self.filename).suffix == ".toml":
51+
self.load_pyproject()
52+
53+
def load_pyproject(self):
54+
with open(self.filename, "rb") as fh:
55+
cp = defaultdict(dict, tomllib.load(fh))
56+
57+
if python := cp["project"].get("requires-python"):
58+
add_requirement("python" + python, self.requirements)
59+
60+
for dep in cp.get("project").get("dependencies"):
61+
add_requirement(dep, self.requirements)
62+
63+
for dep in cp.get("build-system").get("requires"):
64+
add_requirement(dep, self.build_system)
65+
66+
def _get_dependencies(self, include_build_system: bool = True):
67+
"""Get the default conda environment entries."""
68+
69+
reqs = self.requirements.copy()
70+
if include_build_system:
71+
reqs = combine_requirements(reqs, self.build_system)
72+
73+
_python = reqs.pop("python", None)
74+
75+
deps = [str(r) for r in reqs.values() if r.name not in self.pip_packages]
76+
deps.sort(key=str.lower)
77+
if _python:
78+
deps = [str(_python)] + deps
79+
80+
pip = [str(r) for r in reqs.values() if r.name in self.pip_packages]
81+
pip.sort(key=str.lower)
82+
83+
return deps, pip
84+
85+
def export(
86+
self,
87+
outfile: str | Path = "environment.yaml",
88+
include_build_system: bool = True,
89+
):
90+
deps, pip = self._get_dependencies(include_build_system=include_build_system)
91+
92+
conda_env = {"channels": self.channels, "dependencies": deps.copy()}
93+
if pip:
94+
conda_env["dependencies"] += ["pip", {"pip": pip}]
95+
96+
if outfile is None:
97+
return conda_env
98+
99+
p = Path(outfile)
100+
if p.suffix in [".txt"]:
101+
deps += pip
102+
deps.sort(key=str.lower)
103+
with open(p, "w") as outfile:
104+
outfile.writelines("\n".join(deps))
105+
else:
106+
if p.suffix not in [".yaml", ".yml"]:
107+
warnings.warn(
108+
f"Unknown environment format `{p.suffix}`, generating conda yaml output."
109+
)
110+
with open(p, "w") as outfile:
111+
yaml.dump(conda_env, outfile, default_flow_style=False)

0 commit comments

Comments
 (0)