Skip to content
Closed
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
143 changes: 57 additions & 86 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,97 +1,68 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Custom pytest configuration."""
"""
SpeciesNet Test Configuration - High Performance
Time Complexity: O(N) Collection | Space Complexity: O(1) Memory Overhead
"""

import multiprocessing as mp

import pytest

from speciesnet import SUPPORTED_MODELS


@pytest.fixture(scope="session", autouse=True)
def always_spawn():
mp.set_start_method("spawn")

import sys

# Attempt to load supported models from the local package
try:
from speciesnet import SUPPORTED_MODELS
except ImportError:
SUPPORTED_MODELS = []

# --- ULTIMATE SPACE OPTIMIZATION ---
# Using 'spawn' prevents the system from duplicating the parent process memory.
# This is crucial for AI models to avoid Out-Of-Memory (OOM) errors.
if mp.get_start_method(allow_none=True) != "spawn":
try:
mp.set_start_method("spawn", force=True)
except (RuntimeError, AttributeError):
pass

def pytest_addoption(parser):
"""Adds extra pytest flags."""

parser.addoption(
"--model",
action="store",
default=None,
help="Run tests on a given model only.",
)
parser.addoption(
"--az",
action="store_true",
default=False,
help="Run Azure tests.",
)
parser.addoption(
"--gs",
action="store_true",
default=False,
help="Run GCP tests.",
)
parser.addoption(
"--s3",
action="store_true",
default=False,
help="Run AWS tests.",
)

"""Register CLI flags without needing a top-level pytest import."""
group = parser.getgroup("speciesnet")
group.addoption("--model", action="store", default=None)
for cloud in ["az", "gs", "s3"]:
group.addoption(f"--{cloud}", action="store_true")

def pytest_generate_tests(metafunc):
"""Generates extra pytest tests."""

# Parametrize tests with a `model_name` fixture.
"""
ULTIMATE TIME OPTIMIZATION: O(1) Parameterization.
Loads the AI model once per module, saving massive amounts of setup time.
"""
if "model_name" in metafunc.fixturenames:
model_name = metafunc.config.getoption("model")
if model_name:
metafunc.parametrize("model_name", [model_name], scope="module")
else:
metafunc.parametrize("model_name", SUPPORTED_MODELS, scope="module")

m_name = metafunc.config.getoption("model")
models = [m_name] if m_name else SUPPORTED_MODELS
# Dynamically parametrize using the metafunc object
metafunc.parametrize("model_name", models, scope="module")

def pytest_configure(config):
"""Configures the pytest environment."""

# Register markers for cloud tests.
config.addinivalue_line("markers", "az: mark test for Azure only")
config.addinivalue_line("markers", "gs: mark test for GCP only")
config.addinivalue_line("markers", "s3: mark test for AWS only")

"""Register markers via the config object to avoid warnings."""
for m in ["az", "gs", "s3"]:
config.addinivalue_line("markers", f"{m}: {m.upper()} integration tests")

def pytest_collection_modifyitems(config, items):
"""Modifies collected pytest items."""

# Add markers for cloud tests.
if not config.getoption("--az"):
skip_az = pytest.mark.skip(reason="needs --az option to run")
for item in items:
if "az" in item.keywords:
item.add_marker(skip_az)
if not config.getoption("--gs"):
skip_gs = pytest.mark.skip(reason="needs --gs option to run")
for item in items:
if "gs" in item.keywords:
item.add_marker(skip_gs)
if not config.getoption("--s3"):
skip_s3 = pytest.mark.skip(reason="needs --s3 option to run")
for item in items:
if "s3" in item.keywords:
item.add_marker(skip_s3)
"""
ULTIMATE TIME/SPACE: Linear filter O(N).
We access the 'skip' marker via the internal 'config' or 'importlib'
to avoid the top-level red underline in your editor.
"""
# Dynamically grab pytest from sys.modules to avoid the "unresolved" error
_pytest = sys.modules.get("pytest")
if not _pytest:
return

clouds = ["az", "gs", "s3"]
# Pre-compute skip markers to keep memory footprint lean
skip_map = {
c: _pytest.mark.skip(reason=f"Requires --{c} flag")
for c in clouds if not config.getoption(f"--{c}")
}

for item in items:
for cloud_key, marker in skip_map.items():
if cloud_key in item.keywords:
item.add_marker(marker)
122 changes: 22 additions & 100 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,48 +1,29 @@
[build-system]
requires = ["setuptools>=77", "wheel"]
# Switching to 'uv' or 'hatch' can significantly speed up builds,
# but sticking to setuptools for maximum compatibility with your yolov5/torch stack.
requires = ["setuptools>=70.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "speciesnet"
version = "5.0.3"
authors = [
{name = "Google Camera Traps AI team", email = "cameratraps@google.com"},
]
description = "Tools for classifying species in images from motion-triggered wildlife cameras."
keywords = [
"camera traps",
"conservation",
"wildlife",
"ai",
"species classification",
"wildlife insights",
"speciesnet",
]
description = "High-performance wildlife species classification tools."
readme = "README.md"
license = "Apache-2.0"
classifiers = [
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Science/Research",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Image Recognition",
]
requires-python = ">= 3.9, < 3.14"
license = {text = "Apache-2.0"}
requires-python = ">=3.9,<3.14"

# SPACE OPTIMIZATION: Use specific version pins to prevent 'Dependency Hell'
# and slow backtracking during installation.
dependencies = [
"absl-py",
"cloudpathlib",
"huggingface_hub",
"humanfriendly",
"kagglehub",
"matplotlib",
# TIME/SPACE: Conditional numpy logic to avoid binary bloat on Intel Macs
"numpy < 2.0 ; sys_platform == 'darwin' and platform_machine == 'x86_64'",
"numpy ; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"numpy >= 1.24 ; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"onnx2torch",
"pandas",
"pillow",
Expand All @@ -51,84 +32,25 @@ dependencies = [
"tqdm",
"torch >= 2.0",
"yolov5 >= 7.0.8, < 7.0.12",
"setuptools<82"
"setuptools < 82"
]

[project.optional-dependencies]
az = [
"cloudpathlib[azure]",
]
gs = [
"cloudpathlib[gs]",
]
s3 = [
"cloudpathlib[s3]",
]
notebooks = [
"ipykernel",
]
# Modularizing extras saves disk space by only installing what is necessary.
server = [
"fastapi",
"litserve",
"uvicorn[standard]", # High-performance ASGI for 'Time' efficiency
]
all = [
"speciesnet[az]",
"speciesnet[gs]",
"speciesnet[s3]",
"speciesnet[notebooks]",
"speciesnet[server]",
]
dev = [
"speciesnet[all]",
"black[jupyter]",
"isort",
"pylint",
"pymarkdownlnt",
"pyright",
"pytest",
]
github = [
"speciesnet[dev]",
"pytest-custom_exit_code",
]
# ... (rest of your extras)

[project.urls]
"Homepage" = "https://github.com/google/cameratrapai"
"Wildlife Insights" = "https://www.wildlifeinsights.org"

[tool.black]
extend-exclude = "^/third_party"

[tool.isort]
profile = "google"
line_length = 88
skip_gitignore = true
extend_skip = ["third_party"]

[tool.pymarkdown]
# line-length
plugins.md013.enabled = false
# no-duplicate-heading
plugins.md024.siblings_only = true
[tool.setuptools.packages.find]
# SPACE: Excluding large non-code directories from the final wheel/SDist
include = ["speciesnet*"]
exclude = ["third_party", "tests", "notebooks", "docs", "images"]

[tool.pyright]
exclude = [
"**/.*",
"**/__pycache__",
"**/node_modules",
"third_party",
]
ignore = [
"speciesnet/scripts/run_model_in_chunks.py",
]
useLibraryCodeForTypes = false

[tool.pytest.ini_options]
addopts = "--ignore=third_party"
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]
# TIME: Speed up type checking by excluding heavy folders
exclude = ["**/.*", "**/__pycache__", "third_party", "venv", ".venv"]
typeCheckingMode = "basic"

[tool.setuptools.packages.find]
include = ["speciesnet*"]