From 5b5db3a37e37f0ddfc15b3e12193157b28d3341a Mon Sep 17 00:00:00 2001 From: Andrew Mitchell Date: Wed, 6 Nov 2024 20:43:38 +0000 Subject: [PATCH] Refactor dependencies and reorganize project structure (#100) * refactor: remove schema dep * refactor: remove pandas performance deps Seems a better policy to allow users to decide whether or not to use pandas[performance], especially since it can introduce issues with numba, llvmlite as compiled packages. * chore: reorganise pyproject and use uv's new dev groups * chore: check optional installs on testpypi * bump version v0.7.5 -> v0.7.6dev0 * fix: improve optional dependency handling - Implement lazy loading for optional components via __getattr__ - Centralize optional dependency configuration in _optionals.py - Add explicit __all__ list for better IDE support - Remove dynamic __all__ manipulation for more reliable behavior - Maintain helpful error messages for missing dependencies This change improves the developer and user experience when working with optional features while maintaining backwards compatibility. * refactor: relative imports * refactor: reduce dependencies * fix: opt dep tests * bump version v0.7.6dev0 -> v0.7.6dev1 * fix: fixed test collection * bump version v0.7.6dev1 -> v0.7.6dev2 ### Improvements to optional dependencies handling: * Centralized optional dependency configuration in `_optionals.py` for better maintainability. (`CONTRIBUTING.md` [[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055R85)], [[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L93-R99)], [[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L108-R125)], [[4]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L130-R141)], [[5]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L145-R272)]) * Improved error messages and IDE support for optional dependencies. (`CHANGELOG.md` [[CHANGELOG.mdR8-R59](diffhunk://#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4edR8-R59)]) ### Updates to testing and CI workflows: * Added new workflows for tagged releases and testing tagged releases, including installation from TestPyPI and running tests. (`.github/workflows/test-tag-release.yml` [[.github/workflows/test-tag-release.ymlL107-R132](diffhunk://#diff-11b7dedbf7b09ab5a0bd90aa70d8a2eda1918dab64a511c82104706cfa09f3b7L107-R132)]) * Updated `conftest.py` to handle dependency checks in a more modular way, caching results for better performance. (`conftest.py` [[1]](diffhunk://#diff-a31c7ed5d35f5ed8233994868c54d625b18e6bacb6794344c4531e62bd9dde59L7-R39)], [[2]](diffhunk://#diff-a31c7ed5d35f5ed8233994868c54d625b18e6bacb6794344c4531e62bd9dde59L54-R65)], [[3]](diffhunk://#diff-a31c7ed5d35f5ed8233994868c54d625b18e6bacb6794344c4531e62bd9dde59L89-R88)]) * Simplified the `pyproject.toml` by consolidating dependencies and updating test configurations. (`pyproject.toml` [[1]](diffhunk://#diff-50c86b7ed8ac2cf95bd48334961bf0530cdc77b5a56f852c5c61b89d735fd711L3-R15)], [[2]](diffhunk://#diff-50c86b7ed8ac2cf95bd48334961bf0530cdc77b5a56f852c5c61b89d735fd711L37-R57)], [[3]](diffhunk://#diff-50c86b7ed8ac2cf95bd48334961bf0530cdc77b5a56f852c5c61b89d735fd711L71-R66)]) ### Documentation updates: * Added detailed developer notes and guidelines for adding new optional features and handling dependencies. (`CONTRIBUTING.md` [[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055R85)], [[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L93-R99)], [[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L108-R125)], [[4]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L130-R141)], [[5]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L145-R272)]) * Documented the new optional dependencies system and provided examples for both success and failure cases. (`CHANGELOG.md` [[CHANGELOG.mdR8-R59](diffhunk://#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4edR8-R59)]) --- .github/workflows/test-tag-release.yml | 22 +- CHANGELOG.md | 52 ++++ CONTRIBUTING.md | 267 ++++++++------------ conftest.py | 75 +++--- pyproject.toml | 77 +++--- src/soundscapy/__init__.py | 25 +- src/soundscapy/_optionals.py | 86 +++---- src/soundscapy/audio/binaural.py | 4 +- src/soundscapy/audio/metrics.py | 2 +- src/soundscapy/audio/parallel_processing.py | 6 +- test/test__optionals.py | 53 ++-- uv.lock | 149 +++-------- 12 files changed, 387 insertions(+), 431 deletions(-) diff --git a/.github/workflows/test-tag-release.yml b/.github/workflows/test-tag-release.yml index 79a4dd5..762ff35 100644 --- a/.github/workflows/test-tag-release.yml +++ b/.github/workflows/test-tag-release.yml @@ -104,11 +104,29 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' - - name: Install package from TestPyPI + - name: Install soundscapy from TestPyPI uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 3 retry_wait_seconds: 30 command: python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "soundscapy==${{ needs.details.outputs.version_str }}" - - run: python -c "import soundscapy; print(soundscapy.__version__)" \ No newline at end of file + - run: python -c "import soundscapy; print(soundscapy.__version__)" + + - name: Install soundscapy[audio] from TestPyPI + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 30 + command: python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "soundscapy[audio]==${{ needs.details.outputs.version_str }}" + - run: python -c "import soundscapy; print(soundscapy.__version__); from soundscapy import Binaural" + + - name: Install soundscapy[all] from TestPyPI + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 30 + command: python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "soundscapy[all]==${{ needs.details.outputs.version_str }}" + - run: python -c "import soundscapy; print(soundscapy.__version__); from soundscapy import Binaural" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4397084..0f51bcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,58 @@ All notable changes to the Soundscapy project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.6rc0] - 2024-11-06 + +### Changed + +- Improved handling of optional dependencies to provide better error messages and IDE support +- Audio components (like `Binaural`) can now be imported directly from the top-level package + (`from soundscapy import Binaural`) while maintaining helpful error messages when + dependencies are missing +- Centralized optional dependency configuration in `_optionals.py` for better maintainability + +### Developer Notes + +- No changes required to existing code using audio components +- The new system provides better IDE completion support while maintaining the same runtime behavior +- Optional components can still be imported from their original location + (`from soundscapy.audio import Binaural`) or from the top level + (`from soundscapy import Binaural`) + +## [0.7.5] + +### Added + +#### Support for Optional Dependencies + +Soundscapy splits its functionality into optional modules to reduce the number of dependencies required for basic functionality. By default, Soundscapy includes the survey data processing and plotting functionality. + +If you would like to use the binaural audio processing and psychoacoustics functionality, you will need to install the optional `audio` dependency: + +```bash +pip install soundscapy[audio] +``` + +To install all optional dependencies, use the following command: + +```bash +pip install soundscapy[all] +``` + +### Developer notes + +#### Dev Container Configuration + +* Added a new `devcontainer.json` file to configure the development container with specific features and VSCode extensions. (`.devcontainer/devcontainer.json` [.devcontainer/devcontainer.jsonR1-R69](diffhunk://#diff-24ad71c8613ddcf6fd23818cb3bb477a1fb6d83af4550b0bad43099813088686R1-R69)) +* Updated `.dockerignore` to exclude the virtual environment directory. (`.devcontainer/.dockerignore` [.devcontainer/.dockerignoreR1](diffhunk://#diff-7691e653179b9ed2292151d962426f76e6f5378e4989e741859bdfcbcef16b97R1)) + +#### GitHub Workflows: + +* Removed old CI, release, and test-release workflows. (`.github/workflows/ci.yml` [[1]](diffhunk://#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL1-L40) `.github/workflows/release.yml` [[2]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L1-L33) `.github/workflows/test-release.yml` [[3]](diffhunk://#diff-191bb5b4e97db48c9d0bdb945dd00e17b53249422f60a642e9e8d73250b5913aL1-L53) +* Added a new workflow for tagged releases to automate the release process, including building and publishing to PyPI and TestPyPI. (`.github/workflows/tag-release.yml` [.github/workflows/tag-release.ymlR1-R138](diffhunk://#diff-21e1251c1676ed10064d2d98ab1a8f6471a9718058bd316970abe934169f2b60R1-R138)) +* Added a new workflow for testing tagged releases, including installation from TestPyPI and running tests. (`.github/workflows/test-tag-release.yml` [.github/workflows/test-tag-release.ymlR1-R114](diffhunk://#diff-11b7dedbf7b09ab5a0bd90aa70d8a2eda1918dab64a511c82104706cfa09f3b7R1-R114)) +* Added new workflows for running tests on the main codebase and tutorial notebooks. (`.github/workflows/test.yml` [[1]](diffhunk://#diff-faff1af3d8ff408964a57b2e475f69a6b7c7b71c9978cccc8f471798caac2c88R1-R52) `.github/workflows/test-tutorials.yml` [[2]](diffhunk://#diff-01bd86ab14c3e8d7d1382e5ed2172404eb7d3c46bbffeffe09fc11431885e2a0R1-R42) + ## [0.7.3] ### Improved diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00cce0e..11ef9a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,37 +81,48 @@ The avilable types are: 1. **Dependency Definitions** (`_optionals.py`): - ```python - OPTIONAL_DEPENDENCIES = { - "audio": { - "packages": ("mosqito", "maad", "acoustics"), - "install": "soundscapy[audio]", - "description": "audio analysis functionality", - }, - } - - def require_dependencies(group: str) -> Dict[str, Any]: - """Import and return all packages required for a dependency group.""" - ``` + ```python + # Package dependencies + OPTIONAL_DEPENDENCIES = { + "audio": { + "packages": ("mosqito", "maad", "acoustics"), + "install": "soundscapy[audio]", + "description": "audio analysis functionality", + }, + } + + # Top-level imports available when dependencies are installed + OPTIONAL_IMPORTS = { + 'Binaural': ('soundscapy.audio', 'Binaural'), + 'AudioAnalysis': ('soundscapy.audio', 'AudioAnalysis'), + # ... other optional components + } + ``` 2. **Package Configuration** (`pyproject.toml`): - ```toml - [project.optional-dependencies] - audio = [ - "mosqito>=1.2.1", - "scikit-maad>=1.4.3", - "acoustics>=0.2.5", - ] - ``` + ```toml + [project.optional-dependencies] + audio = [ + "mosqito>=1.2.1", + "scikit-maad>=1.4.3", + "acoustics>=0.2.5", + ] + ``` + +3. **Module-Level Dependency Check** (`audio/__init__.py`): -3. **Test Configuration** (`conftest.py`): - The root conftest.py handles: - - Collection control (skipping modules when dependencies are missing) - - Environment variables for doctests - - Test markers for optional dependencies + ```python + from soundscapy._optionals import require_dependencies + + # This will raise an ImportError if dependencies are missing + required = require_dependencies("audio") + + # Now import module components + from .binaural import Binaural + ``` -### Adding Optional Features +### Adding New Optional Features #### 1. Add New Dependencies @@ -127,7 +138,7 @@ uv add package1 package2 --optional new_group #### 2. Update Dependency Definitions -In `_optionals.py`: +In `_optionals.py`, update both dependency mappings: ```python OPTIONAL_DEPENDENCIES = { @@ -142,177 +153,123 @@ OPTIONAL_DEPENDENCIES = { "description": "description of functionality", }, } -``` - -#### 3. Update Test Collection -If adding a new dependency group that requires its own module (like audio/), update the collection check in conftest.py: - -```python -def pytest_ignore_collect(collection_path): - """Control test collection for optional dependency modules.""" - path_str = str(collection_path) - - # Add new module paths here - if "soundscapy/audio/" in path_str: - return not _check_audio_deps() - elif "soundscapy/new_group/" in path_str: - return not _check_new_group_deps() - - return False +OPTIONAL_IMPORTS = { + # Existing imports... + 'NewFeature': ('soundscapy.new_group', 'NewFeature'), # Add new top-level imports +} ``` -#### 4. Implement Feature Code +#### 3. Implement Feature Code -In your module's docstrings, you only need to skip examples that would fail even with dependencies (like missing files or settings): +Create a new module directory if needed (e.g., `new_group/`) with an `__init__.py`: ```python -"""Module docstring with examples. - -Examples --------- ->>> from soundscapy.audio import Feature ->>> feature = Feature.from_file("audio.wav") # doctest: +SKIP ->>> feature.sampling_rate -44100 -""" +"""Module docstring describing the new functionality.""" from soundscapy._optionals import require_dependencies # This will raise an ImportError if dependencies are missing -required = require_dependencies("group_name") +required = require_dependencies("new_group") # Now import your feature code -from .feature import FeatureClass -``` - -The module's doctests don't need special handling for dependencies because: +from .feature import NewFeature -1. If dependencies are missing, the entire module is skipped during collection -2. If dependencies are present, all doctests in the module will run -3. Only use `doctest: +SKIP` for examples that need external files or resources - -Note: REQUIRES directives are only needed for doctests outside the optional module that need to handle both success and failure cases. +__all__ = ["NewFeature"] +``` -### 5. Add Tests +#### 4. Add to Top-Level Exports -For tests outside the optional module, mark them with the optional_deps decorator: +If you want the new feature to be available at the top level, add it to `__all__` in `soundscapy/__init__.py`: ```python -import pytest - -@pytest.mark.optional_deps('audio') -def test_feature(): - from soundscapy.audio import Feature - ... - -# Tests within the optional module don't need markers - they're handled by collection +__all__ = [ + # Core modules... + # Optional modules + "NewFeature", # Add new feature here +] ``` -#### Where to Use xdoctest REQUIRES Directives +### How It Works -The `xdoctest: +REQUIRES(env:AUDIO_DEPS=='1')` directive is only needed in specific cases: +The system provides three levels of dependency handling: -1. **DO NOT USE** in optional module files (e.g., `audio/*.py`): - - ```python - # In audio/feature.py - """ - >>> from soundscapy.audio import Feature # No REQUIRES needed - >>> feature = Feature() # doctest: +SKIP (only if needs external resources) - """ - ``` +1. **Module Level**: The `require_dependencies()` check in the optional module's `__init__.py` ensures dependencies are available before the module is imported. -1. **DO USE** in files outside optional modules that import them: +2. **Top Level Imports**: `__getattr__` in the main `__init__.py` enables importing optional components directly from `soundscapy` with proper error handling: ```python - # In soundscapy/core.py - """ - >>> # xdoctest: +REQUIRES(env:AUDIO_DEPS=='1') - >>> from soundscapy.audio import Feature - >>> feature = Feature() - """ + from soundscapy import Binaural # Works with deps, helpful error without ``` -1. **DO USE** when demonstrating dependency error handling: +3. **IDE Support**: The explicit `__all__` list in `__init__.py` provides IDE autocompletion while maintaining proper runtime behavior. - ```python - # In _optionals.py or other core files - """ - >>> # xdoctest: +REQUIRES(env:AUDIO_DEPS=='0') - >>> from soundscapy._optionals import require_dependencies - >>> try: - ... require_dependencies("audio") - ... except ImportError as e: - ... print(str(e)) - audio analysis functionality requires additional dependencies... - """ - ``` +Benefits: -This is because: +- Clear error messages when dependencies are missing +- Optional components available at both module and package level +- Good IDE support through explicit exports +- Centralized dependency configuration +- No runtime overhead for unused optional features -- Optional module files are completely skipped during collection if dependencies are missing -- Files outside optional modules are always collected, so they need explicit control over which examples run -- Error handling examples specifically need to run when dependencies are missing +### Testing Optional Dependencies -### How Testing Works +Soundscapy uses a flexible system for testing optional dependencies that allows both local development testing and full integration testing in CI. -The testing system uses several mechanisms to handle optional dependencies: +#### Test Structure -1. **Module Collection**: - - The root conftest.py checks dependencies during collection - - Modules (and their tests) are skipped entirely if dependencies are missing - - This prevents import errors during test collection +Optional dependency tests exist at three levels: -2. **Environment Variables**: - - conftest.py sets environment variables (e.g., AUDIO_DEPS) - - These control which doctests/examples run - - Allows showing both success and failure cases +1. **Optional Module Tests**: Tests within optional modules (e.g., `audio/`) + - Only collected when dependencies are available + - Test actual functionality + - No need for special markers or mocking -3. **Test Markers**: - - Used for tests outside optional modules - - Allow granular control over which tests run - - Helpful for integration tests +2. **Integration Tests**: Tests that use optional features from other modules + - Use `@pytest.mark.optional_deps('group')` marker + - Skip when dependencies unavailable + - Test actual integration between components -### Running Tests -The test system **should** automatically: +#### When to Use Each Testing Approach -- Skip collecting modules with missing dependencies -- Run appropriate doctests based on available dependencies -- Skip marked tests when dependencies are missing +1. **Use `@pytest.mark.optional_deps` when**: + - Testing actual functionality that requires dependencies + - Writing integration tests + - Testing with real package interactions -### Best Practices +2. **No special handling needed when**: + - Writing tests within an optional module + - Testing core functionality that doesn't use optional features -1. **Dependency Management**: - - Keep dependencies minimal and logical - - Group related dependencies together - - Document dependencies in pyproject.toml and _optionals.py +### Adding Tests for New Optional Features -2. **Error Messages**: - - Use require_dependencies() for consistent error messages - - Include installation instructions in error messages +When adding new optional features: -3. **Testing**: - - Add both positive and negative doctest examples - - Use markers only for tests outside optional modules - - Test with and without dependencies installed +1. **Inside Optional Module**: + - Put tests in the module's test directory + - No special handling needed + - Tests will only run when dependencies are available -4. **Documentation**: - - Document which features require which dependencies - - Show examples for both success and failure cases - - Keep doctests up to date with actual functionality - -5. **Code Organization**: - - Keep optional features in their own modules - - Handle dependencies at module boundaries - - Follow existing code style (Ruff formatter) + ```python + # soundscapy/new_group/tests/test_feature.py + def test_new_feature(): + """Regular test, no special handling needed.""" + from soundscapy.new_group import NewFeature + assert NewFeature.method() == expected + ``` -### Additional Development Guidelines +2. **Integration Tests**: + - Use the optional_deps marker + - Put in main test directory -- Run tests with dependencies installed and without -- Update docstrings when changing dependencies -- Follow existing code style -- Keep the dependency system documentation updated + ```python + # test/test_integration.py + @pytest.mark.optional_deps('new_group') + def test_new_feature_integration(): + """Will skip if dependencies missing.""" + from soundscapy import NewFeature + assert NewFeature.integrate() == expected + ``` ## Github Actions diff --git a/conftest.py b/conftest.py index cc7a020..04a04d4 100644 --- a/conftest.py +++ b/conftest.py @@ -4,66 +4,65 @@ import os from loguru import logger from _pytest.logging import LogCaptureFixture -from soundscapy._optionals import require_dependencies -# Cache the dependency check result -_has_audio = None +# Cache the dependency check results +_dependency_cache = {} -def _check_audio_deps(): - """Check for audio dependencies, caching the result.""" - global _has_audio - if _has_audio is None: +def _check_dependencies(group: str) -> bool: + """Check for dependencies of a group, caching the result.""" + if group not in _dependency_cache: try: - required = require_dependencies("audio") - logger.debug(f"Audio dependencies found: {list(required.keys())}") - _has_audio = True + from soundscapy._optionals import require_dependencies + + required = require_dependencies(group) + logger.debug(f"{group} dependencies found: {list(required.keys())}") + _dependency_cache[group] = True except ImportError as e: - logger.debug(f"Missing audio dependencies: {e}") - _has_audio = False - logger.debug(f"Setting AUDIO_DEPS={_has_audio}") - return _has_audio + logger.debug(f"Missing {group} dependencies: {e}") + _dependency_cache[group] = False + return _dependency_cache[group] def pytest_ignore_collect(collection_path): - """Control test collection for optional dependency modules. + """Control test collection for optional dependency modules.""" + path_str = str(collection_path) - Parameters - ---------- - collection_path : Path - Path to the file being considered for collection + # Map module paths to their dependency groups + module_deps = { + "audio/": "audio", + # Add new optional module paths here + } - Returns - ------- - bool - True if the file should be ignored, False otherwise - """ - path_str = str(collection_path) - # Check if path is in the audio module - if "audio/" in path_str: - # Skip collection if audio dependencies are missing - should_ignore = not _check_audio_deps() - logger.debug(f"Collection check for {path_str}: ignore={should_ignore}") - return should_ignore + for module_path, dep_group in module_deps.items(): + if module_path in path_str: + should_ignore = not _check_dependencies(dep_group) + logger.debug(f"Collection check for {path_str}: ignore={should_ignore}") + return should_ignore return None def pytest_configure(config): """Register markers and configure test environment.""" - # Register only necessary markers config.addinivalue_line( "markers", "optional_deps(group): mark tests requiring optional dependencies" ) - # Set environment variable for xdoctest - os.environ["AUDIO_DEPS"] = "1" if _check_audio_deps() else "0" + # Set environment variables for each dependency group + from soundscapy._optionals import OPTIONAL_DEPENDENCIES + + for group in OPTIONAL_DEPENDENCIES: + env_var = f"{group.upper()}_DEPS" + os.environ[env_var] = "1" if _check_dependencies(group) else "0" + logger.debug(f"Set {env_var}={os.environ[env_var]}") - # Configure xdoctest namespace - config.option.xdoctest_namespace = """ + # Configure xdoctest namespace with all dependency groups + namespace_setup = """ import os from soundscapy._optionals import require_dependencies """ + config.option.xdoctest_namespace = namespace_setup @pytest.fixture @@ -86,7 +85,5 @@ def pytest_runtest_setup(item): group = marker.args[0] if marker.args else marker.kwargs.get("group") if not group: pytest.fail("No dependency group specified for optional_deps marker") - try: - require_dependencies(group) - except ImportError: + if not _check_dependencies(group): pytest.skip(f"Missing optional dependencies for {group}") diff --git a/pyproject.toml b/pyproject.toml index b4ba9bf..e80d90d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,18 @@ [project] name = "soundscapy" -version = "0.7.5" +version = "0.7.6dev2" description = "A python library for analysing and visualising soundscape assessments." authors = [ { name = "Andrew Mitchell", email = "a.j.mitchell@ucl.ac.uk" } ] dependencies = [ - "pandas[excel,performance]>=2.2.2", + "pandas[excel]>=2.2.2", "seaborn>=0.13.2", - "pyyaml>=6.0.2", - "schema>=0.7.7", - "loguru>=0.7.2", - "pydantic>=2.8.2", "plotly>=5.23.0", "scipy>=1.14.1", + "pyyaml>=6.0.2", + "pydantic>=2.8.2", + "loguru>=0.7.2", ] readme = "README.md" requires-python = ">= 3.10" @@ -34,32 +33,28 @@ classifiers = [ repository = "https://github.com/MitchellAcoustics/Soundscapy" documentation = "https://soundscapy.readthedocs.io/en/latest/" -[tool.pytest.ini_options] -addopts = "-v --tb=short --durations=5 --xdoctest -n 6 --cov=src/soundscapy --cov-report=term" -testpaths = ["test", "src/soundscapy"] -python_files = "test_*.py" -python_classes = "Test*" -python_functions = "test_*" -console_output_style = "count" -doctest_optionflags = "NUMBER NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL" -markers = [ - "optional_deps(group): mark tests that depend on optional dependencies. group can be 'audio', etc.", - "slow: mark test as slow", - "skip: mark test as skipped", - "skipif: mark test as skipped if condition is met", - "xfail: mark test as expected to fail", - "parametrize: mark test as parametrized" -] - [project.optional-dependencies] +all = [ + "soundscapy[audio]", +] audio = [ "mosqito>=1.2.1", "scikit-maad>=1.4.3", "tqdm>=4.66.5", "acoustics>=0.2.5", + "numba>=0.59", # subdep of mosqito, pinned to avoid numba python version issues +] + +[tool.uv] +default-groups = ["dev", "test"] + +[dependency-groups] +dev = [ + "bumpver>=2023.1129", + "ruff>=0.7.2", ] test = [ - "pytest>=8.3.2", + "pytest>=8.3.3", "setuptools>=72.1.0", "nbmake>=1.5.4", "pytest-xdist>=3.6.1", @@ -68,7 +63,7 @@ test = [ "pytest-cov>=6.0.0", ] docs = [ - "jupyter>=1.0.0", + "jupyter>=1.1.1", "mkdocs>=1.6.0", "mkdocs-material>=9.5.31", "mkdocs-jupyter>=0.24.8", @@ -78,19 +73,29 @@ docs = [ "jupyter-dash>=0.4.2", ] -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +[tool.uv.pip] +universal = true -[tool.uv] -managed = true -dev-dependencies = [ - "bumpver>=2023.1129", - "ruff>=0.7.2", +[tool.pytest.ini_options] +addopts = "-v --tb=short --durations=5 --xdoctest -n 6 --cov=src/soundscapy --cov-report=term" +testpaths = ["test", "src/soundscapy"] +python_files = "test_*.py" +python_classes = "Test*" +python_functions = "test_*" +console_output_style = "count" +doctest_optionflags = "NUMBER NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL" +markers = [ + "optional_deps(group): mark tests that depend on optional dependencies. group can be 'audio', etc.", + "slow: mark test as slow", + "skip: mark test as skipped", + "skipif: mark test as skipped if condition is met", + "xfail: mark test as expected to fail", + "parametrize: mark test as parametrized" ] -[tool.uv.pip] -universal = true +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" [tool.hatch.metadata] allow-direct-references = true @@ -116,7 +121,7 @@ exclude = [ ] [tool.bumpver] -current_version = "v0.7.5" +current_version = "v0.7.6dev2" version_pattern = "vMAJOR.MINOR.PATCH[[-]PYTAGNUM]" commit_message = "bump version {old_version} -> {new_version}" tag_message = "{new_version}" diff --git a/src/soundscapy/__init__.py b/src/soundscapy/__init__.py index fc5accd..6b6f09d 100644 --- a/src/soundscapy/__init__.py +++ b/src/soundscapy/__init__.py @@ -3,6 +3,7 @@ """ # ruff: noqa: E402 +from typing import Any from loguru import logger # https://loguru.readthedocs.io/en/latest/resources/recipes.html#configuring-loguru-to-be-used-by-a-library-or-an-application @@ -12,16 +13,19 @@ __version__ = importlib.metadata.version("soundscapy") +from soundscapy._optionals import import_optional + +# Always available core modules from soundscapy import surveys from soundscapy import databases from soundscapy import plotting - from soundscapy.logging import setup_logging from soundscapy.databases import araus, isd, satp from soundscapy.surveys import processing from soundscapy.plotting import scatter_plot, density_plot __all__ = [ + # Core modules "surveys", "databases", "plotting", @@ -32,13 +36,18 @@ "scatter_plot", "density_plot", "setup_logging", + # Optional modules listed explicitly for IDE/typing support + "Binaural", + "AudioAnalysis", + "AnalysisSettings", + "ConfigManager", + "process_all_metrics", + "prep_multiindex_df", + "add_results", + "parallel_process", ] -try: - from soundscapy import audio - - __all__.extend(["audio"]) - __all__.extend(audio.__all__) -except ImportError: - logger.debug("Audio module not available") +def __getattr__(name: str) -> Any: + """Lazy import handling for optional components.""" + return import_optional(name) diff --git a/src/soundscapy/_optionals.py b/src/soundscapy/_optionals.py index cb476ac..03f284d 100644 --- a/src/soundscapy/_optionals.py +++ b/src/soundscapy/_optionals.py @@ -4,59 +4,6 @@ This module provides utilities for managing optional dependencies across the package. It allows graceful handling of missing dependencies and provides helpful feedback about which dependencies are missing and how to install them. - -Examples --------- -Basic usage - importing optional dependencies: - ->>> from soundscapy._optionals import require_dependencies ->>> # xdoctest: +SKIP ->>> required = require_dependencies("audio") # Returns dict of imported modules ->>> mosqito = required["mosqito"] # Access specific module - -Error handling for missing dependencies: - ->>> # xdoctest: +REQUIRES(env:AUDIO_DEPS=0) ->>> from soundscapy._optionals import require_dependencies ->>> try: -... required = require_dependencies("audio") -... except ImportError as e: -... print(str(e)) # doctest: +ELLIPSIS -audio analysis functionality requires additional dependencies. Install with: pip install soundscapy[audio] - -Successful dependency loading: - ->>> # xdoctest: +REQUIRES(env:AUDIO_DEPS==1) ->>> from soundscapy._optionals import require_dependencies ->>> required = require_dependencies("audio") ->>> isinstance(required, dict) -True ->>> 'mosqito' in required -True - -Typical usage in a module: - ->>> # xdoctest: +SKIP ->>> from soundscapy._optionals import require_dependencies ->>> # This will raise ImportError with helpful message if dependencies missing ->>> required = require_dependencies("audio") ->>> # Now import feature code that depends on the optional packages ->>> from .binaural import Binaural ->>> from .analysis_settings import AnalysisSettings - -Notes ------ -The `require_dependencies()` function is the main interface for managing optional -dependencies. It performs these key functions: - -1. Checks if all required packages for a feature group are available -2. Returns a dictionary of imported modules if successful -3. Raises an ImportError with installation instructions if dependencies are missing - -The module uses OPTIONAL_DEPENDENCIES to define feature groups and their requirements: - - packages: Tuple of required package names - - install: pip install command/target - - description: Human-readable feature description """ from typing import Dict, Any @@ -79,6 +26,17 @@ description (str): Human-readable feature description """ +OPTIONAL_IMPORTS = { + "Binaural": ("soundscapy.audio", "Binaural"), + "AudioAnalysis": ("soundscapy.audio", "AudioAnalysis"), + "AnalysisSettings": ("soundscapy.audio", "AnalysisSettings"), + "ConfigManager": ("soundscapy.audio", "ConfigManager"), + "process_all_metrics": ("soundscapy.audio", "process_all_metrics"), + "prep_multiindex_df": ("soundscapy.audio", "prep_multiindex_df"), + "add_results": ("soundscapy.audio", "add_results"), + "parallel_process": ("soundscapy.audio", "parallel_process"), +} + def require_dependencies(group: str) -> Dict[str, Any]: """Import and return all packages required for a dependency group. @@ -110,5 +68,23 @@ def require_dependencies(group: str) -> Dict[str, Any]: return packages except ImportError as e: raise ImportError( - f"Missing optional dependency for {e.name}. Install with: pip install {OPTIONAL_DEPENDENCIES[group]['install']}" - ) + f"{OPTIONAL_DEPENDENCIES[group]['description']} requires additional dependencies. " + f"Install with: pip install {OPTIONAL_DEPENDENCIES[group]['install']}" + ) from e + + +def import_optional(name: str) -> Any: + """Import an optional component by name.""" + if name not in OPTIONAL_IMPORTS: + raise AttributeError(f"module 'soundscapy' has no attribute '{name}'") + + module_name, attr_name = OPTIONAL_IMPORTS[name] + try: + module = importlib.import_module(module_name) + return getattr(module, attr_name) + except ImportError as e: + group = "audio" # Can be made dynamic if we add more groups + raise ImportError( + f"The {name} component requires {OPTIONAL_DEPENDENCIES[group]['description']}. " + f"Install with: pip install {OPTIONAL_DEPENDENCIES[group]['install']}" + ) from e diff --git a/src/soundscapy/audio/binaural.py b/src/soundscapy/audio/binaural.py index ed36b36..f8eba99 100644 --- a/src/soundscapy/audio/binaural.py +++ b/src/soundscapy/audio/binaural.py @@ -39,8 +39,8 @@ import scipy.signal from acoustics import Signal -from soundscapy.audio.analysis_settings import AnalysisSettings, MetricSettings -from soundscapy.audio.metrics import ( +from .analysis_settings import AnalysisSettings, MetricSettings +from .metrics import ( maad_metric_1ch, maad_metric_2ch, mosqito_metric_1ch, diff --git a/src/soundscapy/audio/metrics.py b/src/soundscapy/audio/metrics.py index fac853c..659103d 100644 --- a/src/soundscapy/audio/metrics.py +++ b/src/soundscapy/audio/metrics.py @@ -44,7 +44,7 @@ ) from scipy import stats -from soundscapy.audio.analysis_settings import AnalysisSettings +from .analysis_settings import AnalysisSettings DEFAULT_LABELS = { "LZeq": "LZeq", diff --git a/src/soundscapy/audio/parallel_processing.py b/src/soundscapy/audio/parallel_processing.py index 01344fe..9d06aa7 100644 --- a/src/soundscapy/audio/parallel_processing.py +++ b/src/soundscapy/audio/parallel_processing.py @@ -24,9 +24,9 @@ import pandas as pd from tqdm.auto import tqdm -from soundscapy.audio.analysis_settings import AnalysisSettings -from soundscapy.audio.binaural import Binaural -from soundscapy.audio.metrics import ( +from .analysis_settings import AnalysisSettings +from .binaural import Binaural +from .metrics import ( add_results, prep_multiindex_df, process_all_metrics, diff --git a/test/test__optionals.py b/test/test__optionals.py index 772b98f..0814b1e 100644 --- a/test/test__optionals.py +++ b/test/test__optionals.py @@ -32,7 +32,7 @@ def test_require_dependencies_missing(): with pytest.raises(ImportError) as exc_info: require_dependencies("audio") - assert "Missing optional dependency" in str(exc_info.value) + assert "Install with:" in str(exc_info.value) assert "pip install soundscapy[audio]" in str(exc_info.value) @@ -47,26 +47,41 @@ def test_require_dependencies_present(): assert all(packages[pkg] is not None for pkg in packages) -def test_audio_module_import_behavior(): - """Test top-level audio module import behavior.""" +def test_optional_import_behavior(): + """Test top-level optional component import behavior.""" import importlib import soundscapy - - # Check if audio module is in __all__ - has_audio = "audio" in soundscapy.__all__ - - # Should match whether dependencies are available - try: - # Attempt to import dependencies - importlib.import_module("mosqito") - importlib.import_module("maad") - importlib.import_module("acoustics") - - # If all imports succeed, audio module should be present - assert has_audio is True - except ImportError: - # If any import fails, audio module should not be present - assert has_audio is False + from soundscapy._optionals import OPTIONAL_IMPORTS + + # Test that optional components are listed in __all__ + for name in OPTIONAL_IMPORTS: + assert name in soundscapy.__all__ + + # Test import behavior + def has_audio_deps(): + """Check if audio dependencies are available.""" + try: + importlib.import_module("mosqito") + importlib.import_module("maad") + importlib.import_module("acoustics") + return True + except ImportError: + return False + + # Try importing Binaural as an example component + if has_audio_deps(): + # Should succeed when dependencies are available + from soundscapy import Binaural + + assert hasattr(Binaural, "__module__") + else: + # Should raise helpful ImportError when dependencies missing + import pytest + + with pytest.raises(ImportError) as exc_info: + from soundscapy import Binaural + assert "audio analysis functionality" in str(exc_info.value) + assert "pip install soundscapy[audio]" in str(exc_info.value) def test_invalid_group(): diff --git a/uv.lock b/uv.lock index 7763d7b..b78f4b0 100644 --- a/uv.lock +++ b/uv.lock @@ -184,45 +184,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", size = 9456 }, ] -[[package]] -name = "bottleneck" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/61/9fb34409d58f04e1929da41666a055c36f9495903ff669b80c893bdee65f/bottleneck-1.4.2.tar.gz", hash = "sha256:fa8e8e1799dea5483ce6669462660f9d9a95649f6f98a80d315b84ec89f449f4", size = 103563 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/f3/7e76090a8ab7f2d5f123ba6cad556c7c324bcef2320b1aa3e6a8f87c0f1d/Bottleneck-1.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:125436df93751a226eab1732783aa8f6125e88e779587aa61be071fb66e41f9d", size = 98563 }, - { url = "https://files.pythonhosted.org/packages/b7/db/5a600f6c071e93284e8480684b971a7cce334d9e6b6d57386cc391537d14/Bottleneck-1.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c6df9a60ec6ab88fec934ca864266ba95edd89c490af71dc9cd8afb2a54ebd9", size = 360776 }, - { url = "https://files.pythonhosted.org/packages/e3/8f/8d0322287dd208bd35b2814152726d6f7ec9346c9ad2abae18e23e9ef15e/Bottleneck-1.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2fe327dc2d0564e295a5857a252755103f8c6e05b07d3ff80a69afaa9f5065", size = 356085 }, - { url = "https://files.pythonhosted.org/packages/20/1b/05dd0433052f62b416d3af4d58556f377518b1d35f76872c53e79bd7818f/Bottleneck-1.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6b7790ca8658cd69e3cc0d0e4ff0e9829d60849bf7945fbd7344fbce05b2bbb8", size = 365247 }, - { url = "https://files.pythonhosted.org/packages/b6/6b/eb7a04afa8d4641a498b62a24db5a491ab3d6945890e9f5d5f852ba0aa8c/Bottleneck-1.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6282fa925ac3768f66e3547f89a512376d3f9de7ef53bdd37aa29232fd864054", size = 356080 }, - { url = "https://files.pythonhosted.org/packages/4e/91/53353689ed860403f421900ec0ce67dfa763bd39d07d9da5b69c48b3941a/Bottleneck-1.4.2-cp310-cp310-win32.whl", hash = "sha256:e56a206fbf48e3b8054a964398bf1ed843e9625d3c6bdbeb7898cb48bf97441b", size = 106941 }, - { url = "https://files.pythonhosted.org/packages/d7/25/32643c8e8646f30121e5c67a0c0579dbc910f3bf9e121683f28165c6d374/Bottleneck-1.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:eb0c611d15b0fd8f511d288e8964e4725b4b3b0d9d310880cf0ff6b8dd03c859", size = 111622 }, - { url = "https://files.pythonhosted.org/packages/88/b8/31a1cc8279bf11a60c04b844a42666927307a47bb48964cbd92ec9f40e3e/Bottleneck-1.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6902ebf3e85315b481bc084f10c5770f8240275ad1e039ac69c7c8d2013b040", size = 98565 }, - { url = "https://files.pythonhosted.org/packages/16/64/09d72babae7cc29341c52f2e9381066672743d4f797c86b1e735205d5fc8/Bottleneck-1.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2fd34b9b490204f95288f0dd35d37042486a95029617246c88c0f94a0ab49fe", size = 364986 }, - { url = "https://files.pythonhosted.org/packages/7e/d6/39e957e9df9ab16df9c531e8ddf71594877063d27aa036dd105b66d3b3b3/Bottleneck-1.4.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122845e3106c85465551d4a9a3777841347cfedfbebb3aa985cca110e07030b1", size = 360256 }, - { url = "https://files.pythonhosted.org/packages/ff/cb/d287febe0e6504194ba94cf4a6d80df66a0031ca33a32b30f00c030238cc/Bottleneck-1.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1f61658ebdf5a178298544336b65020730bf86cc092dab5f6579a99a86bd888b", size = 369507 }, - { url = "https://files.pythonhosted.org/packages/dc/1e/9310f058ddee71798a76ab15c5c1ad71f0a5c3c6348f7faab9b6da038484/Bottleneck-1.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c7d29c044a3511b36fd744503c3e697e279c273a8477a6d91a2831d04fd19e0", size = 360282 }, - { url = "https://files.pythonhosted.org/packages/96/cb/c1f2a37e86e9fa47845259f0a8f32d550f7f27b908432369de055be9f7c4/Bottleneck-1.4.2-cp311-cp311-win32.whl", hash = "sha256:c663cbba8f52011fd82ee08c6a85c93b34b19e0e7ebba322d2d67809f34e0597", size = 106936 }, - { url = "https://files.pythonhosted.org/packages/d3/eb/3fd23404bbc612cf9e4883c3c2b359bd14528e234d5c40bb29bcfd591ef8/Bottleneck-1.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:89651ef18c06616850203bf8875c958c5d316ea48d8ba60d9b450199d39ae391", size = 111617 }, - { url = "https://files.pythonhosted.org/packages/d2/26/6f5124e31a67f75e2a3b9239cc382145326e91fc45e7d7bc9ebffa05fdfa/Bottleneck-1.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a74ddd0417f42eeaba37375f0fc065b28451e0fba45cb2f99e88880b10b3fa43", size = 98681 }, - { url = "https://files.pythonhosted.org/packages/c4/93/e100b6eda77f2aecf5f16157b8c04dd3463913ba188b582650cd77ccf42b/Bottleneck-1.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:070d22f2f62ab81297380a89492cca931e4d9443fa4b84c2baeb52db09c3b1b4", size = 365422 }, - { url = "https://files.pythonhosted.org/packages/82/2b/c6fea2bb048d04c13b8564052818a198d50ce58d5f439ec69c2b0c458703/Bottleneck-1.4.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fc4e7645bd425c05e05acd5541e9e09cb4179e71164e862f082561bf4509eac", size = 361844 }, - { url = "https://files.pythonhosted.org/packages/8f/4c/811475885bd60cf0cb28822568d0c0c3c7d7de4fbccd2ebb66863e7dc726/Bottleneck-1.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:037315c56605128a39f77d19af6a6019dc8c21a63694a4bfef3c026ed963be2e", size = 370369 }, - { url = "https://files.pythonhosted.org/packages/fd/ee/0a8157e6bbd2168bf6171811534a5a73a35f54c453dd7d86a323773b5bd7/Bottleneck-1.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99778329331d5fae8df19772a019e8b73ba4d9d1650f110cd995ab7657114db0", size = 361786 }, - { url = "https://files.pythonhosted.org/packages/fa/6b/e8fda0510b8fa0f3f9a3586efc941abe9d546198e95ae5690c3c83370b36/Bottleneck-1.4.2-cp312-cp312-win32.whl", hash = "sha256:7363b3c8ce6ca433779cd7e96bcb94c0e516dcacadff0011adcbf0b3ac86bc9d", size = 107149 }, - { url = "https://files.pythonhosted.org/packages/22/25/908b75a329a05b82d717661aa95a1968d9dae0e68c654d5e16bfe0d6fbb6/Bottleneck-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:48c6b9d9287c4102b803fcb01ae66ae7ef6b310b711b4b7b7e23bf952894dc05", size = 111766 }, - { url = "https://files.pythonhosted.org/packages/2e/65/148e146ca8c16af9881a0db1d8d1849d49a5186fc9f065c79a8d25d6fc0c/Bottleneck-1.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c1c885ad02a6a8fa1f7ee9099f29b9d4c03eb1da2c7ab25839482d5cce739021", size = 98701 }, - { url = "https://files.pythonhosted.org/packages/80/96/6540ac9a9943b0d6f0199eddbde55e878f970d2bdda31207dc3e7a195c2b/Bottleneck-1.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7a1b023de1de3d84b18826462718fba548fed41870df44354f9ab6a414ea82f", size = 365443 }, - { url = "https://files.pythonhosted.org/packages/d0/aa/ccae264aac3b2621fa8a98c7afe033f22a352467cbf85fa2799d176ec31b/Bottleneck-1.4.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c9dbaf737b605b30c81611f2c1d197c2fd2e46c33f605876c1d332d3360c4fc", size = 361849 }, - { url = "https://files.pythonhosted.org/packages/f3/b3/5f96d7bb23a291b835bf0a34eec359c55613f6c4262ad1bb161d897499c0/Bottleneck-1.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7ebbcbe5d4062e37507b9a81e2aacdb1fcccc6193f7feff124ef2b5a6a5eb740", size = 370654 }, - { url = "https://files.pythonhosted.org/packages/51/05/9d1ababa3fd34014b708351270307320c0bc595d2d66c2ba2b9b92f0d618/Bottleneck-1.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:964f6ac4118ddab3bbbac79d4f726b093459be751baba73ee0aa364666e8068e", size = 362054 }, - { url = "https://files.pythonhosted.org/packages/92/e3/123488804830604432f84a2c43e611b8e1971e230b9466a7315850d22a58/Bottleneck-1.4.2-cp313-cp313-win32.whl", hash = "sha256:2db287f6ecdbb1c998085eca9b717fec2bfc48a4ab6ae070a9820ba8ab59c90b", size = 107160 }, - { url = "https://files.pythonhosted.org/packages/54/f0/e1640ccd8468c61693092f38f835ef35a68a1ea72c3388683148b3800aa6/Bottleneck-1.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:26b5f0531f7044befaad95c20365dd666372e66bdacbfaf009ff65d60285534d", size = 111774 }, -] - [[package]] name = "bumpver" version = "2023.1129" @@ -1877,38 +1838,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588 }, ] -[[package]] -name = "numexpr" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/61/dd156698c6211a877170303b49be711f6952570c2a03bfdd2ae3217fb213/numexpr-2.10.1.tar.gz", hash = "sha256:9bba99d354a65f1a008ab8b87f07d84404c668e66bab624df5b6b5373403cf81", size = 101580 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/e3/7fcce966d3447535f534d5d9d63260060c64062f0b1240d9aa86e2a2ff9c/numexpr-2.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbd35f17f6efc00ebd4a480192af1ee30996094a0d5343b131b0e90e61e8b554", size = 141365 }, - { url = "https://files.pythonhosted.org/packages/1d/92/07a85f90737df22abf0355a86790eb698015e6886332a89765f78d188ff7/numexpr-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fecdf4bf3c1250e56583db0a4a80382a259ba4c2e1efa13e04ed43f0938071f5", size = 130975 }, - { url = "https://files.pythonhosted.org/packages/0b/43/05ccb35fef17d4c88fb6f079807b9aef93cf58bf738385ac1197e008ab58/numexpr-2.10.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2efa499f460124538a5b4f1bf2e77b28eb443ee244cc5573ed0f6a069ebc635", size = 402895 }, - { url = "https://files.pythonhosted.org/packages/0e/e2/f07ab8322b1cd207e562b417c746fb006367f190955ba1dbf17981bcfcd9/numexpr-2.10.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac23a72eff10f928f23b147bdeb0f1b774e862abe332fc9bf4837e9f1bc0bbf9", size = 405008 }, - { url = "https://files.pythonhosted.org/packages/8c/c3/bae733aa8e4acda51613dd1a039ca05d0b2d0544a593ccf4f7b8761e8679/numexpr-2.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b28eaf45f1cc1048aad9e90e3a8ada1aef58c5f8155a85267dc781b37998c046", size = 1375671 }, - { url = "https://files.pythonhosted.org/packages/00/99/6f3d06548abf29af0d0398be6a33adc81de2f3e10e3735449b219bf6be75/numexpr-2.10.1-cp310-cp310-win32.whl", hash = "sha256:4f0985bd1c493b23b5aad7d81fa174798f3812efb78d14844194834c9fee38b8", size = 148087 }, - { url = "https://files.pythonhosted.org/packages/6a/6e/c1db0f99c04240cff8a80334a50c74708bdd90b319b87799867f1f84bca9/numexpr-2.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:44f6d12a8c44be90199bbb10d3abf467f88951f48a3d1fbbd3c219d121f39c9d", size = 141168 }, - { url = "https://files.pythonhosted.org/packages/98/a2/b72bf13afc815e64779a671c880a9e2753ae62cff10d3a1548b5ba6cf5e8/numexpr-2.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3c0b0bf165b2d886eb981afa4e77873ca076f5d51c491c4d7b8fc10f17c876f", size = 141382 }, - { url = "https://files.pythonhosted.org/packages/ae/dd/2e64a90d5632cc8ceb964f84c801d896411a37edc563def94e2e9873e630/numexpr-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56648a04679063175681195670ad53e5c8ca19668166ed13875199b5600089c7", size = 130940 }, - { url = "https://files.pythonhosted.org/packages/d9/32/1100b44ca545ea969204daf260f747eebea71758f13236374f7a5ef759e1/numexpr-2.10.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce04ae6efe2a9d0be1a0e114115c3ae70c68b8b8fbc615c5c55c15704b01e6a4", size = 404509 }, - { url = "https://files.pythonhosted.org/packages/2f/b3/55ac60a4003fcc6013ef87a3c97fd7811c5662cb1c19ea438c03601c3d8f/numexpr-2.10.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45f598182b4f5c153222e47d5163c3bee8d5ebcaee7e56dd2a5898d4d97e4473", size = 406661 }, - { url = "https://files.pythonhosted.org/packages/02/89/9b78120ddaf9b4615a7a9ebaffaf93e0931aec0a219e72b7ade9823280a8/numexpr-2.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a50370bea77ba94c3734a44781c716751354c6bfda2d369af3aed3d67d42871", size = 1377483 }, - { url = "https://files.pythonhosted.org/packages/e1/a7/2d6e4832a23142ccaefe26c3f0e901fd782dd73a30eaef6f78f387cc9f52/numexpr-2.10.1-cp311-cp311-win32.whl", hash = "sha256:fa4009d84a8e6e21790e718a80a22d57fe7f215283576ef2adc4183f7247f3c7", size = 148088 }, - { url = "https://files.pythonhosted.org/packages/06/c6/da8f79ce916598d82d3240318f282eb70120c95dab0e61ece1c67c41434f/numexpr-2.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:fcbf013bb8494e8ef1d11fa3457827c1571c6a3153982d709e5d17594999d4dd", size = 141170 }, - { url = "https://files.pythonhosted.org/packages/c1/29/6470c540591f0f19529dd1ffb3c45dd1ca0a413317ba6b9441609f0dc370/numexpr-2.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82fc95c301b15ff4823f98989ee363a2d5555d16a7cfd3710e98ddee726eaaaa", size = 141591 }, - { url = "https://files.pythonhosted.org/packages/1f/09/d6e9d502ec37c3a8bdecd45b8c7ddd61c19076649616e285f8d6196dafb5/numexpr-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbf79fef834f88607f977ab9867061dcd9b40ccb08bb28547c6dc6c73e560895", size = 131084 }, - { url = "https://files.pythonhosted.org/packages/48/0c/35fc08876bbfef03a7ed083c92041a28d71fabfab81a202e46a8ebe772aa/numexpr-2.10.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:552c8d4b2e3b87cdb2abb40a781b9a61a9090a9f66ac7357fc5a0b93aff76be3", size = 405802 }, - { url = "https://files.pythonhosted.org/packages/04/ad/5c9c9943323692e1027484b95f2fe5166c36eb71039d4133ebd29f2569d2/numexpr-2.10.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22cc65e9121aeb3187a2b50827715b2b087ea70e8ab21416ea52662322087b43", size = 408540 }, - { url = "https://files.pythonhosted.org/packages/be/c7/4ceec83eb39931d7132bf218b84b26efacadd32b022042ffa407050962c5/numexpr-2.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:00204e5853713b5eba5f3d0bc586a5d8d07f76011b597c8b4087592cc2ec2928", size = 1379067 }, - { url = "https://files.pythonhosted.org/packages/c2/0e/c6a4c67ad3e3aec2da87b3c300363a2b3c9a689f6ba67a4b71f444240989/numexpr-2.10.1-cp312-cp312-win32.whl", hash = "sha256:82bf04a1495ac475de4ab49fbe0a3a2710ed3fd1a00bc03847316b5d7602402d", size = 148228 }, - { url = "https://files.pythonhosted.org/packages/f5/4f/6c2621db459b8cab5cfcbd21c6ccc6cde594d0256853d9a79e4f454ed131/numexpr-2.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:300e577b3c006dd7a8270f1bb2e8a00ee15bf235b1650fe2a6febec2954bc2c3", size = 141256 }, -] - [[package]] name = "numpy" version = "2.0.2" @@ -2052,11 +1981,6 @@ excel = [ { name = "xlrd" }, { name = "xlsxwriter" }, ] -performance = [ - { name = "bottleneck" }, - { name = "numba" }, - { name = "numexpr" }, -] [[package]] name = "pandocfilters" @@ -2956,15 +2880,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/7b/c920673ac01c19814dd15fc617c02301c522f3d6812ca2024f4588ed4549/ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760", size = 8735845 }, ] -[[package]] -name = "schema" -version = "0.7.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/01/0ea2e66bad2f13271e93b729c653747614784d3ebde219679e41ccdceecd/schema-0.7.7.tar.gz", hash = "sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807", size = 44245 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/1b/81855a88c6db2b114d5b2e9f96339190d5ee4d1b981d217fa32127bb00e0/schema-0.7.7-py2.py3-none-any.whl", hash = "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde", size = 18632 }, -] - [[package]] name = "scikit-image" version = "0.24.0" @@ -3110,26 +3025,39 @@ wheels = [ [[package]] name = "soundscapy" -version = "0.7.5rc5" +version = "0.7.6.dev0" source = { editable = "." } dependencies = [ { name = "loguru" }, - { name = "pandas", extra = ["excel", "performance"] }, + { name = "pandas", extra = ["excel"] }, { name = "plotly" }, { name = "pydantic" }, { name = "pyyaml" }, - { name = "schema" }, { name = "scipy" }, { name = "seaborn" }, ] [package.optional-dependencies] +all = [ + { name = "acoustics" }, + { name = "mosqito" }, + { name = "numba" }, + { name = "scikit-maad" }, + { name = "tqdm" }, +] audio = [ { name = "acoustics" }, { name = "mosqito" }, + { name = "numba" }, { name = "scikit-maad" }, { name = "tqdm" }, ] + +[package.dev-dependencies] +dev = [ + { name = "bumpver" }, + { name = "ruff" }, +] docs = [ { name = "ipywidgets" }, { name = "jupyter" }, @@ -3150,41 +3078,21 @@ test = [ { name = "xdoctest", extra = ["all"] }, ] -[package.dev-dependencies] -dev = [ - { name = "bumpver" }, - { name = "ruff" }, -] - [package.metadata] requires-dist = [ { name = "acoustics", marker = "extra == 'audio'", specifier = ">=0.2.5" }, - { name = "ipywidgets", marker = "extra == 'docs'", specifier = ">=8.1.3" }, - { name = "jupyter", marker = "extra == 'docs'", specifier = ">=1.0.0" }, - { name = "jupyter-dash", marker = "extra == 'docs'", specifier = ">=0.4.2" }, { name = "loguru", specifier = ">=0.7.2" }, - { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.0" }, - { name = "mkdocs-jupyter", marker = "extra == 'docs'", specifier = ">=0.24.8" }, - { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5.31" }, - { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.25.2" }, { name = "mosqito", marker = "extra == 'audio'", specifier = ">=1.2.1" }, - { name = "nbmake", marker = "extra == 'test'", specifier = ">=1.5.4" }, - { name = "pandas", extras = ["excel", "performance"], specifier = ">=2.2.2" }, + { name = "numba", marker = "extra == 'audio'", specifier = ">=0.59" }, + { name = "pandas", extras = ["excel"], specifier = ">=2.2.2" }, { name = "plotly", specifier = ">=5.23.0" }, { name = "pydantic", specifier = ">=2.8.2" }, - { name = "pymdown-extensions", marker = "extra == 'docs'", specifier = ">=10.9" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.2" }, - { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=6.0.0" }, - { name = "pytest-mpl", marker = "extra == 'test'", specifier = ">=0.17.0" }, - { name = "pytest-xdist", marker = "extra == 'test'", specifier = ">=3.6.1" }, { name = "pyyaml", specifier = ">=6.0.2" }, - { name = "schema", specifier = ">=0.7.7" }, { name = "scikit-maad", marker = "extra == 'audio'", specifier = ">=1.4.3" }, { name = "scipy", specifier = ">=1.14.1" }, { name = "seaborn", specifier = ">=0.13.2" }, - { name = "setuptools", marker = "extra == 'test'", specifier = ">=72.1.0" }, + { name = "soundscapy", extras = ["audio"], marker = "extra == 'all'" }, { name = "tqdm", marker = "extra == 'audio'", specifier = ">=4.66.5" }, - { name = "xdoctest", extras = ["all"], marker = "extra == 'test'", specifier = ">=1.1.6" }, ] [package.metadata.requires-dev] @@ -3192,6 +3100,25 @@ dev = [ { name = "bumpver", specifier = ">=2023.1129" }, { name = "ruff", specifier = ">=0.7.2" }, ] +docs = [ + { name = "ipywidgets", specifier = ">=8.1.3" }, + { name = "jupyter", specifier = ">=1.1.1" }, + { name = "jupyter-dash", specifier = ">=0.4.2" }, + { name = "mkdocs", specifier = ">=1.6.0" }, + { name = "mkdocs-jupyter", specifier = ">=0.24.8" }, + { name = "mkdocs-material", specifier = ">=9.5.31" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.25.2" }, + { name = "pymdown-extensions", specifier = ">=10.9" }, +] +test = [ + { name = "nbmake", specifier = ">=1.5.4" }, + { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-mpl", specifier = ">=0.17.0" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "setuptools", specifier = ">=72.1.0" }, + { name = "xdoctest", extras = ["all"], specifier = ">=1.1.6" }, +] [[package]] name = "soupsieve"