Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New #2

Merged
merged 4 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ jobs:
run: pip install -r ./code_check_req.txt
- name: run_code_check
run: bash code_check.sh

unit_tests:
runs-on: ubuntu-20.04
steps:
- uses: actions/[email protected]
with:
python-version: 3.11
- uses: conda-incubator/setup-miniconda@v3
with:
python-version: 3.11
- name: install test requirements
run: pip install -r ./tests/requirements.txt
- name: run pytest
run: pytest -v
Binary file not shown.
Binary file not shown.
73 changes: 33 additions & 40 deletions conda_dependency_cleaner/_clean_environment_from_file.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,8 @@
from dataclasses import dataclass, field

from conda import exports as ce
from conda.env.env import Environment, from_file
from conda.exports import linked
from conda.models.dist import Dist

from ._get_dependeny_graph import get_dependency_graph
from ._to_yaml_patch import to_yaml_patch


@dataclass
class _Dependency:
full_name: str
exclude_version: bool
exclude_build: bool

name: str = field(init=False)
version: str = field(init=False)
build: str = field(init=False)

def __post_init__(self) -> None:
"""After init process the full name."""
self.name, rest = self.full_name.split("==")
self.version, self.build = rest.split("=")

def __repr__(self) -> str:
"""
Define the representation of the Dependency.

:return: Return the name.
"""
v = "" if self.exclude_version else f"=={self.version}"
b = "" if (self.exclude_build or self.exclude_version) else f"={self.build}"
return f"{self.name}{v}{b}"
from .utility import Dependency, get_dependency_graph, to_yaml_patch


def clean_environment_from_file(
Expand All @@ -49,21 +20,43 @@ def clean_environment_from_file(
:param exclude_build: Whether to remove the builds of the dependencies.
"""
env: Environment = from_file(environment_file_path)
package_cache: list[Dist] = ce.linked(env.prefix)

package_cache: list[Dist] = linked(env.prefix)
# Generate directed graph from distributions.
graph = get_dependency_graph(packages=package_cache, env_path=env.prefix)
# Extract all packages without ingoing dependencies.
# Extract all packages that are roots (i.e have no packages depend on them).
roots = [k for k, v in graph.in_degree if v < 1]
filtered_dependencies = [
str(_Dependency(d, exclude_version, exclude_build))
for d in env.dependencies["conda"]
if any((n == d.split("==")[0] for n in roots))
]
# Get filtered dependencies for conda and pip
conda_dependencies = _get_filtered_dependencies(
env.dependencies.get("conda"), roots, exclude_version, exclude_build
)

# For now we can only filter conda packages
# TODO: maybe incorporate filtering for pip
pip_deps: list[str] | None = env.dependencies.get("pip")
new_dependencies = conda_dependencies + ([{"pip": pip_deps}] if pip_deps else [])

env_dict = env.to_dict()
env_dict["dependencies"] = filtered_dependencies
env_dict["dependencies"] = new_dependencies

path = new_file_name or env.filename
print(path)
with open(path, "wb") as stream:
to_yaml_patch(stream=stream, obj=env_dict)


def _get_filtered_dependencies(
dependencies: list[str] | None, roots: list[str], ev: bool, eb: bool
) -> list[str]:
"""
Get a list of filtered dependencies.

:param dependencies: The dependencies to filter.
:param roots: The root dependencies.
:param ev: Exclude version from dependency representation.
:param eb: Exclude build from dependency representation.
:return: The filtered list.
"""
if dependencies is None:
return []
dependencies = [Dependency(d, ev, eb) for d in dependencies]
return [repr(d) for d in dependencies if any((n == d.name for n in roots))]
2 changes: 1 addition & 1 deletion conda_dependency_cleaner/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.1"
__version__ = "0.0.2"
6 changes: 2 additions & 4 deletions conda_dependency_cleaner/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ def main() -> None:
parser.add_argument(
"--exclude-version",
help="Allows to exclude version of the dependency.",
action='store_true'
action="store_true",
)
parser.add_argument(
"--exclude-build",
help="Allows to exclude build of the dependency.",
action='store_true'
"--exclude-build", help="Allows to exclude build of the dependency.", action="store_true"
)
parser.add_argument(
"-h",
Expand Down
7 changes: 7 additions & 0 deletions conda_dependency_cleaner/utility/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""A collection of implemented utility functions and Classes."""

from ._dependency import Dependency
from ._get_dependeny_graph import get_dependency_graph
from ._to_yaml_patch import to_yaml_patch

__all__ = ["to_yaml_patch", "get_dependency_graph", "Dependency"]
30 changes: 30 additions & 0 deletions conda_dependency_cleaner/utility/_dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from dataclasses import dataclass, field


@dataclass
class Dependency:
"""A class representing a dependency of the conda environment."""

full_name: str
exclude_version: bool
exclude_build: bool

name: str = field(init=False)
version: str = field(init=False)
build: str = field(init=False)

def __post_init__(self) -> None:
"""After init, process the full name."""
components = self.full_name.split("=")
self.name, self.version, *other = tuple(filter(None, components))
self.build = other[0] if len(other) > 0 else ""

def __repr__(self) -> str:
"""
Define the representation of the Dependency.

:return: Return the name.
"""
v = "" if self.exclude_version else f"=={self.version}"
b = "" if (self.exclude_build or self.exclude_version) else f"={self.build}"
return f"{self.name}{v}{b}"
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ def to_yaml_patch(stream: BinaryIO, obj: dict[str, Any]) -> None:
parser.indent(mapping=2, offset=2, sequence=4)
parser.default_flow_style = False
parser.sort_base_mapping_type_on_output = False

parser.dump(obj, stream)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "conda-dependency-cleaner"
version = "0.0.1"
version = "0.0.2"
description = "Clean your conda yaml file."
readme = "README.md"
license = "MIT"
Expand Down
15 changes: 15 additions & 0 deletions tests/clean_test_env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: CondaDependencyCleanerTest
channels:
- defaults
- https://repo.anaconda.com/pkgs/main
- https://repo.anaconda.com/pkgs/r
dependencies:
- brotli==1.0.9=h5eee18b_8
- matplotlib==3.9.2=py312h06a4308_1
- unicodedata2==15.1.0=py312h5eee18b_0
- pip:
- cycler==0.12.1
- fonttools==4.55.0
- kiwisolver==1.4.7
- networkx==3.4.2
- numpy==2.1.3
2 changes: 2 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest~=8.3.3
pytest-mock==3.14.0
56 changes: 56 additions & 0 deletions tests/test_clean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import subprocess
from unittest.mock import Mock
import tempfile
import os
from conda.env.env import Environment, from_file
from conda_dependency_cleaner.utility import to_yaml_patch


def test_clean(mocker: Mock) -> None:
"""
Test the environment cleaner script.

:param mocker: The mock object.
"""
with tempfile.TemporaryDirectory() as temp_dir:
initial_env_file = f"{os.path.dirname(os.path.realpath(__file__))}/test_env.yml"
clean_env_file = f"{os.path.dirname(os.path.realpath(__file__))}/clean_test_env.yml"
clean_env: Environment = from_file(clean_env_file)

# Create the test environment
base_env_file = f"{temp_dir}/test.yml"
base_env = _conda_operations(initial_env_file, base_env_file)

# Clean the environment
cleaned_env_file = f"{temp_dir}/test_clean.yml"
subprocess.run(["clean-yaml", base_env_file, "-nf", cleaned_env_file])
_remove_conda_env(base_env)

cleaned_env = _conda_operations(cleaned_env_file, cleaned_env_file)
_remove_conda_env(cleaned_env)

assert cleaned_env.dependencies == clean_env.dependencies, "Error: Dependencies are different."


def _conda_operations(initial_env: str, new_env: str) -> Environment:
"""
Create a conda environment based on a environment file, then export it to a new file.


:param initial_env: The environment file.
:param new_env: The new environment file.
:return: The environment loaded as a python object.
"""
env = from_file(initial_env)
subprocess.run(["conda", "env", "create", "-f", initial_env])
subprocess.run(["conda", "env", "export", "-n", env.name, "-f", new_env])
return env


def _remove_conda_env(env: Environment) -> None:
"""
Remove a conda environment from the system.

:param env: The environment to remove.
"""
subprocess.run(["conda", "env", "remove", "-n", env.name, "-y"])
Loading
Loading