diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 0f8b521..5e88599 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -26,4 +26,4 @@ jobs: - name: flake8 run: flake8 . - name: mypy - run: mypy . + run: mypy src && mypy tests diff --git a/pyproject.toml b/pyproject.toml index a5269ff..88890a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ exclude = '^venv*|^.venv*|.git|.eggs|build|dist|.cache|.pytest_cache|.mypy_cache python_version = '3.11' warn_return_any = true warn_unused_configs = true +# Let mypy find modules inside the 'src' directory when importing 'hd_active'. +mypy_path = 'src' # Disable the warning below, from type hinting variables in a function. # By default, the bodies of untyped functions are not checked, consider using --check-untyped-defs disable_error_code = 'annotation-unchecked' diff --git a/setup.cfg b/setup.cfg index 090f019..014c1d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ # Configuration for tools that don't support `pyproject.toml`. [flake8] -exclude = .git,.github,.venv*,venv*,__pycache__,assets,src/hd_active/ui/forms,docs,site +exclude = .git,.github,.venv*,venv*,__pycache__,assets,src/hd_active/ui/forms,docs,site,build max-line-length = 100 # Errors being ignored: diff --git a/src/hd_active/hd_active.py b/src/hd_active/hd_active.py index b85cf63..7767c33 100644 --- a/src/hd_active/hd_active.py +++ b/src/hd_active/hd_active.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Deque, Iterable, Optional, Set, Union -from src.hd_active.hd_action_state import HdActionState +from .hd_action_state import HdActionState FILE_NAME = '_hd_active.txt' logger = logging.getLogger(__name__) @@ -148,7 +148,7 @@ def change_state(self) -> HdActionState: import argparse import sys - from src.hd_active.hd_active_config import HdActiveConfig + from .hd_active_config import HdActiveConfig parser = argparse.ArgumentParser(description='Keep HDs active.') parser.add_argument( diff --git a/src/hd_active/main.py b/src/hd_active/main.py index 3292aba..963f932 100644 --- a/src/hd_active/main.py +++ b/src/hd_active/main.py @@ -2,8 +2,8 @@ from PySide6 import QtGui, QtWidgets -from src.hd_active.ui.system_tray_icon import SystemTrayIcon -from src.hd_active.utils import get_asset +from .ui.system_tray_icon import SystemTrayIcon +from .utils import get_asset def main(): diff --git a/src/hd_active/ui/log_dialog.py b/src/hd_active/ui/log_dialog.py index 3b7a1c8..3a1f4a0 100644 --- a/src/hd_active/ui/log_dialog.py +++ b/src/hd_active/ui/log_dialog.py @@ -1,8 +1,8 @@ from PySide6.QtGui import QShowEvent from PySide6.QtWidgets import QDialog, QWidget -from src.hd_active.hd_active import HdActive -from src.hd_active.ui.forms.ui_log_dialog import Ui_LogDialog +from ..hd_active import HdActive +from .forms.ui_log_dialog import Ui_LogDialog class LogDialog(QDialog, Ui_LogDialog): diff --git a/src/hd_active/ui/settings_dialog.py b/src/hd_active/ui/settings_dialog.py index e394f14..a2fffaa 100644 --- a/src/hd_active/ui/settings_dialog.py +++ b/src/hd_active/ui/settings_dialog.py @@ -1,8 +1,8 @@ from PySide6.QtWidgets import QDialog, QWidget -from src.hd_active.hd_active import HdActive -from src.hd_active.ui.forms.ui_settings_dialog import Ui_SettingsDialog -from src.hd_active.ui.log_dialog import LogDialog +from ..hd_active import HdActive +from .forms.ui_settings_dialog import Ui_SettingsDialog +from .log_dialog import LogDialog class SettingsDialog(QDialog, Ui_SettingsDialog): diff --git a/src/hd_active/ui/system_tray_icon.py b/src/hd_active/ui/system_tray_icon.py index 6b156a8..40d6a68 100644 --- a/src/hd_active/ui/system_tray_icon.py +++ b/src/hd_active/ui/system_tray_icon.py @@ -2,11 +2,11 @@ from PySide6.QtWidgets import QApplication, QMenu, QMessageBox, QSystemTrayIcon -from src.hd_active import __version__ -from src.hd_active.hd_active import HdActive -from src.hd_active.hd_active_config import HdActiveConfig -from src.hd_active.ui.settings_dialog import SettingsDialog -from src.hd_active.utils import is_truthy +from .. import __version__ +from ..hd_active import HdActive +from ..hd_active_config import HdActiveConfig +from ..utils import is_truthy +from .settings_dialog import SettingsDialog HD_ACTION_DEBUG = is_truthy(os.getenv('HD_ACTION_DEBUG', 'True')) """ diff --git a/tasks.py b/tasks.py index 59d8831..9170caf 100644 --- a/tasks.py +++ b/tasks.py @@ -1,4 +1,5 @@ import os +import sys from pathlib import Path from invoke import Collection, Exit, task @@ -14,6 +15,9 @@ """Source code for the this project's package.""" ASSETS_DIR = PROJECT_ROOT / 'assets' +if str(PROJECT_SOURCE_DIR) not in sys.path: + sys.path.insert(0, str(PROJECT_SOURCE_DIR)) + # Requirements files REQUIREMENTS_MAIN = 'main' REQUIREMENTS_FILES = { @@ -123,9 +127,9 @@ def _get_os_name(): def _get_build_app_files() -> tuple[Path, Path]: - import src.hd_active + import hd_active - version = src.hd_active.__version__ + version = hd_active.__version__ # Assumes the distribution directory is empty prior to creating the app files = [f for f in BUILD_DIST_APP_DIR.glob('*') if f.is_file() and f.suffix.lower() != '.zip'] @@ -280,7 +284,19 @@ def _update_imports(): file_path = root_path / file regex_replace = [ - (r'''^( *from[ ]+)(\.)( .*)''', module), # from . import + (r'''^( *from[ ]+)(\.{1})( .*)''', module), # from . import + ( + r'''^( *from[ ]+)(\.{2})( .*)''', + '.'.join(module.split('.')[:-1]), + ), # from .. import + ( + r'''^( *from[ ]+)(\.{3})( .*)''', + '.'.join(module.split('.')[:-2]), + ), # from ... import + ( + r'''^( *from[ ]+)(\.{3})(.*)''', + '.'.join(module.split('.')[:-2]) + '.', + ), ( r'''^( *from[ ]+)(\.{2})(.*)''', '.'.join(module.split('.')[:-1]) + '.', @@ -583,10 +599,10 @@ def build_upload(c, label: str = 'none'): """ from packaging.version import Version - import src.hd_active + import hd_active _, zip_file = _get_build_app_files() - app_version = Version(src.hd_active.__version__) + app_version = Version(hd_active.__version__) if not zip_file.exists(): raise Exit( @@ -644,7 +660,7 @@ def build_run(c): raise Exit('Multiple executables found.') c.run(str(exes[0])) elif os_name == 'mac': - app_file, _, _ = _get_build_app_files() + app_file, _ = _get_build_app_files() c.run(str(app_file)) elif os_name == 'linux': raise Exit('Running on Linux not yet implemented.') @@ -756,12 +772,17 @@ def lint_mypy(c, path='.'): c.run(f'mypy {path}') -@task(lint_isort, lint_black, lint_flake8, lint_mypy) +@task def lint_all(c): """ Run all linters. Config for each of the tools is in ``pyproject.toml`` and ``setup.cfg``. """ + lint_isort(c) + lint_black(c) + lint_flake8(c) + lint_mypy(c, 'src') + lint_mypy(c, 'tests') print('Done') diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3fcbdc8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +# Ensure the 'src' directory is on sys.path so tests can import 'hd_active' without installation. +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / 'src' +if str(SOURCE) not in sys.path: + sys.path.insert(0, str(SOURCE)) diff --git a/tests/test_hd_active.py b/tests/test_hd_active.py index 9670c30..6a65ae4 100644 --- a/tests/test_hd_active.py +++ b/tests/test_hd_active.py @@ -6,8 +6,8 @@ import pytest -from src.hd_active.hd_action_state import HdActionState -from src.hd_active.hd_active import HdActive +from hd_active.hd_action_state import HdActionState +from hd_active.hd_active import HdActive WAIT = 0.1 WAIT_TEST = 2 * WAIT @@ -22,7 +22,7 @@ def __init__(self, drive_paths=None, run=False, wait=WAIT): super().__init__(drive_paths, run, wait=wait) -@patch('src.hd_active.hd_active.HdActive._write_hd', return_value=1000) +@patch('hd_active.hd_active.HdActive._write_hd', return_value=1000) class TestHdActive: def test_instantiate_not_started(self, mock_write_hd): hd_active = HdActiveTest(drive_paths=['z'], run=False) diff --git a/tests/test_hd_active_config.py b/tests/test_hd_active_config.py index 40e4052..619b082 100644 --- a/tests/test_hd_active_config.py +++ b/tests/test_hd_active_config.py @@ -3,7 +3,7 @@ import pytest -from src.hd_active.hd_active_config import HdActiveConfig +from hd_active.hd_active_config import HdActiveConfig @pytest.fixture @@ -19,7 +19,7 @@ def config_file(request, tmp_path) -> Tuple[str, List[str]]: return str(file), request.param[1] -@patch('src.hd_active.hd_active_config.configparser.ConfigParser.read') +@patch('hd_active.hd_active_config.configparser.ConfigParser.read') def test_defaults(read_mock): """ Skip reading file (so defaults are not overwritten) and verify defaults. diff --git a/tests/test_utils.py b/tests/test_utils.py index e99d3c1..7351472 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import pytest -from src.hd_active.utils import ASSETS_ROOT, PROJECT_ROOT, get_asset, is_truthy +from hd_active.utils import ASSETS_ROOT, PROJECT_ROOT, get_asset, is_truthy class TestGlobals: