Skip to content

Commit

Permalink
Add requirements into pyproject.toml & Refactor anomalib install `g…
Browse files Browse the repository at this point in the history
…et_requirements` (#1808)

* Refactor get_requirements function to use importlib.metadata

Signed-off-by: harimkang <[email protected]>

* Add dynamic version setting in pyproject.toml

Signed-off-by: harimkang <[email protected]>

* Add full option dependency in pyproject.toml

Signed-off-by: Kang, Harim <[email protected]>

* Remove gitpython and ipykernel from logger dependencies

---------

Signed-off-by: harimkang <[email protected]>
Signed-off-by: Kang, Harim <[email protected]>
Co-authored-by: Samet Akcay <[email protected]>
  • Loading branch information
harimkang and samet-akcay authored Mar 18, 2024
1 parent a8654ff commit 8c6e607
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 56 deletions.
94 changes: 73 additions & 21 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,66 @@
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",
]
notebooks = ["gitpython", "ipykernel", "ipywidgets", "notebook"]
dev = [
"pre-commit",
"pytest",
"pytest-cov",
"pytest-xdist",
"pytest-mock",
"pytest-sugar",
"coverage[toml]",
"tox",
]
full = ["anomalib[core,openvino,loggers,notebooks]"]

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

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

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# RUFF CONFIGURATION #
Expand Down Expand Up @@ -65,28 +125,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 +222,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 +239,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

0 comments on commit 8c6e607

Please sign in to comment.