Skip to content

Add requirements into pyproject.toml & Refactor anomalib install get_requirements #1808

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

Merged
merged 6 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
95 changes: 74 additions & 21 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,67 @@
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "anomalib"
dynamic = ["version"]
readme = "README.md"
description = "anomalib - Anomaly Detection Library"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [{ name = "Intel OpenVINO" }]

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# REQUIREMENTS #
dependencies = [
"omegaconf>=2.1.1",
"rich>=13.5.2",
"jsonargparse==4.27.1",
"docstring_parser", # CLI help-formatter
"rich_argparse", # CLI help-formatter
]

[project.optional-dependencies]
core = [
"av>=10.0.0",
"einops>=0.3.2",
"freia>=0.2",
"imgaug==0.4.0",
"kornia>=0.6.6,<0.6.10",
"matplotlib>=3.4.3",
"opencv-python>=4.5.3.56",
"pandas>=1.1.0",
"timm>=0.5.4,<=0.6.13",
"lightning>2,<2.2.0",
"torch>=2,<2.2.0",
"torchmetrics==0.10.3",
"open-clip-torch>=2.23.0",
]
openvino = ["openvino-dev>=2023.0", "nncf>=2.5.0", "onnx>=1.13.1"]
loggers = [
"comet-ml>=3.31.7",
"gradio>=4",
"tensorboard",
"wandb==0.12.17",
"GitPython",
"ipykernel",
]
notebooks = ["gitpython", "ipykernel", "ipywidgets", "notebook"]
dev = [
"pre-commit",
"pytest",
"pytest-cov",
"pytest-xdist",
"pytest-mock",
"pytest-sugar",
"coverage[toml]",
"tox",
]

[project.scripts]
anomalib = "anomalib.cli.cli:main"

[tool.setuptools.dynamic]
version = { attr = "anomalib.__version__" }

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# RUFF CONFIGURATION #
Expand Down Expand Up @@ -65,28 +126,28 @@ ignore = [
"D107", # Missing docstring in __init__

# pylint
"PLR0913", # Too many arguments to function call
"PLR2004", # consider replacing with a constant variable
"PLR0912", # Too many branches
"PLR0915", # Too many statements
"PLR0913", # Too many arguments to function call
"PLR2004", # consider replacing with a constant variable
"PLR0912", # Too many branches
"PLR0915", # Too many statements

# flake8-annotations
"ANN101", # Missing-type-self
"ANN002", # Missing type annotation for *args
"ANN003", # Missing type annotation for **kwargs
"ANN101", # Missing-type-self
"ANN002", # Missing type annotation for *args
"ANN003", # Missing type annotation for **kwargs

# flake8-bandit (`S`)
"S101", # Use of assert detected.

# flake8-boolean-trap (`FBT`)
"FBT001", # Boolean positional arg in function definition
"FBT002", # Boolean default value in function definition
"FBT001", # Boolean positional arg in function definition
"FBT002", # Boolean default value in function definition

# flake8-datatimez (`DTZ`)
"DTZ005", #Β The use of `datetime.datetime.now()` without `tz` argument is not allowed
"DTZ005", #Β The use of `datetime.datetime.now()` without `tz` argument is not allowed

# flake8-fixme (`FIX`)
"FIX002", # Line contains TODO, consider resolving the issue
"FIX002", # Line contains TODO, consider resolving the issue
]

# Allow autofix for all enabled rules (when `--fix`) is provided.
Expand Down Expand Up @@ -162,12 +223,7 @@ skips = ["B101"]
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# PYTEST CONFIGURATION #
[tool.pytest.ini_options]
addopts = [
"--strict-markers",
"--strict-config",
"--showlocals",
"-ra",
]
addopts = ["--strict-markers", "--strict-config", "--showlocals", "-ra"]
testpaths = "tests"
pythonpath = "src"

Expand All @@ -184,10 +240,7 @@ exclude_lines = [
]

[tool.coverage.paths]
source = [
"src",
".tox/*/site-packages",
]
source = ["src", ".tox/*/site-packages"]


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
Expand Down
22 changes: 13 additions & 9 deletions src/anomalib/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# SPDX-License-Identifier: Apache-2.0

import logging
from pathlib import Path

from pkg_resources import Requirement
from rich.console import Console
from rich.logging import RichHandler

Expand Down Expand Up @@ -41,21 +41,25 @@ def anomalib_install(option: str = "full", verbose: bool = False) -> int:
"""
from pip._internal.commands import create_command

options = (
[option]
if option != "full"
else [option.stem for option in Path("requirements").glob("*.txt") if option.stem != "dev"]
)
requirements = get_requirements(requirement_files=options)
requirements_dict = get_requirements("anomalib")

requirements = []
if option == "full":
for extra in requirements_dict:
requirements.extend(requirements_dict[extra])
elif option in requirements_dict:
requirements.extend(requirements_dict[option])
elif option is not None:
requirements.append(Requirement.parse(option))

# Parse requirements into torch and other requirements.
# This is done to parse the correct version of torch (cpu/cuda).
torch_requirement, other_requirements = parse_requirements(requirements, skip_torch="core" not in options)
torch_requirement, other_requirements = parse_requirements(requirements, skip_torch=option not in ("full", "core"))

# Get install args for torch to install it from a specific index-url
install_args: list[str] = []
torch_install_args = []
if "core" in options and torch_requirement is not None:
if option in ("full", "core") and torch_requirement is not None:
torch_install_args = get_torch_install_args(torch_requirement)

# Combine torch and other requirements.
Expand Down
47 changes: 27 additions & 20 deletions src/anomalib/cli/utils/installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os
import platform
import re
from importlib.metadata import requires
from pathlib import Path
from warnings import warn

Expand All @@ -23,32 +24,38 @@
}


def get_requirements(requirement_files: list[str]) -> list[Requirement]:
"""Get packages from requirements.txt file.
def get_requirements(module: str = "anomalib") -> dict[str, list[Requirement]]:
"""Get requirements of module from importlib.metadata.

This function returns list of required packages from requirement files.

Args:
requirement_files (list[Requirement]): txt files that contains list of required
packages.
This function returns list of required packages from importlib_metadata.

Example:
>>> get_required_packages(requirement_files=["openvino"])
[Requirement('onnx>=1.8.1'), Requirement('networkx~=2.5'), Requirement('openvino-dev==2021.4.1'), ...]
>>> get_requirements("anomalib")
{
"base": ["jsonargparse==4.27.1", ...],
"core": ["torch==2.1.1", ...],
...
}

Returns:
list[Requirement]: List of required packages
dict[str, list[Requirement]]: List of required packages for each optional-extras.
"""
required_packages: list[Requirement] = []

for requirement_file in requirement_files:
with Path(f"requirements/{requirement_file}.txt").open(encoding="utf8") as file:
for line in file:
package = line.strip()
if package and not package.startswith(("#", "-f")):
required_packages.append(Requirement.parse(package))

return required_packages
requirement_list: list[str] | None = requires(module)
extra_requirement: dict[str, list[Requirement]] = {}
if requirement_list is None:
return extra_requirement
for requirement in requirement_list:
extra = "core"
requirement_extra: list[str] = requirement.replace(" ", "").split(";")
if isinstance(requirement_extra, list) and len(requirement_extra) > 1:
extra = requirement_extra[-1].split("==")[-1].strip("'\"")
_requirement_name = requirement_extra[0]
_requirement = Requirement.parse(_requirement_name)
if extra in extra_requirement:
extra_requirement[extra].append(_requirement)
else:
extra_requirement[extra] = [_requirement]
return extra_requirement


def parse_requirements(
Expand Down
15 changes: 9 additions & 6 deletions tests/unit/cli/test_installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ def requirements_file() -> Path:
return Path(f.name)


def test_get_requirements() -> None:
def test_get_requirements(mocker: MockerFixture) -> None:
"""Test that get_requirements returns the expected dictionary of requirements."""
options = [option.stem for option in Path("requirements").glob("*.txt") if option.stem != "dev"]
requirements = get_requirements(requirement_files=options)
assert isinstance(requirements, list)
requirements = get_requirements("anomalib")
assert isinstance(requirements, dict)
assert len(requirements) > 0
for req in requirements:
assert isinstance(req, Requirement)
for reqs in requirements.values():
assert isinstance(reqs, list)
for req in reqs:
assert isinstance(req, Requirement)
mocker.patch("anomalib.cli.utils.installation.requires", return_value=None)
assert get_requirements() == {}


def test_parse_requirements() -> None:
Expand Down