Note: This document is a working implementation guide and will not be committed
to the repository. All permanent documentation will be in reStructuredText format
under doc/source/contributor/.
This document provides a concrete, phased implementation plan for code quality improvements. The plan is structured around deliverable outcomes rather than incremental tooling, with each phase being self-contained, self-testing, and self-documenting.
- Deliverable-Focused: Each phase delivers working, tested improvements
- Self-Testing: CI enforcement added with each code change; tests restructured with fixture pattern (Phase 0.2)
- Self-Documenting: RST documentation updated as part of each phase
- Non-Breaking: All changes maintain backward compatibility and test stability
- Pragmatic: Scope appropriate to repository size (~43 Python files)
- Consistent: Test structure and fixture pattern aligned with main Watcher project
Before starting Phase 0, verify:
- All existing tests pass:
tox -e py3 - PEP8 checks pass:
tox -e pep8 - Coverage baseline established:
tox -e cover - Current branch is clean:
git status
Goal: Configure linters to support (but not yet enforce) import policies.
Duration: 30 minutes
Scope: Add per-file ignores for the allowed star import exception.
Files to modify:
pyproject.toml
Changes:
[tool.ruff.lint.per-file-ignores]
"watcher_dashboard/test/*" = ["S"] # Existing security rule ignore
"watcher_dashboard/test/settings.py" = ["F403", "F405"] # NEW: explicit star import exceptionRationale:
- F403: Unable to detect undefined names from star imports
- F405: Name may be undefined or defined from star imports
- Only
watcher_dashboard/test/settings.pyis permitted to use star imports per policy
Tasks:
- Add explicit F403/F405 ignore for test/settings.py
- Add inline comment documenting this as the sole exception
- Verify Ruff still catches violations elsewhere
Testing:
# Verify Ruff accepts the configuration
ruff check watcher_dashboard/test/settings.py
# Should pass without F403/F405 errors
# Verify it catches violations elsewhere
echo 'from os import *' > /tmp/test_violation.py
ruff check /tmp/test_violation.py
# Should report F403/F405 errors
# Verify all existing checks still pass
tox -e pep8Documentation:
- Add brief inline comment in
pyproject.tomlexplaining the configuration - Update commit message explaining the exception and rationale
Acceptance:
tox -e pep8passes- Ruff configuration validates
- Per-file ignore documented with rationale
Goal: Reorganize test directory to support multiple test suites and reusable fixtures.
Duration: 1-2 hours
Rationale: Prepare test infrastructure for fixture pattern adoption. While functional and gating tests won't be added to this repository, adopting the structural pattern from the main Watcher project enables consistent patterns and reusable fixtures.
Scope: Move existing tests to unit/ subdirectory and create fixture infrastructure.
Current Structure:
watcher_dashboard/test/
├── __init__.py
├── settings.py
├── test_*.py (various unit tests)
├── api_tests/
├── content_tests/
└── ...
Target Structure:
watcher_dashboard/test/
├── __init__.py
├── settings.py
├── unit/ # All existing unit tests (moved here)
│ ├── __init__.py
│ ├── test_config.py # Will be created in Phase 3
│ ├── api/
│ │ ├── __init__.py
│ │ └── test_*.py
│ └── content/
│ ├── __init__.py
│ └── test_*.py
├── local_fixtures/ # Reusable test fixtures
│ ├── __init__.py
│ └── config.py # Config-related fixtures
└── playwright/ # Placeholder for future browser tests
└── __init__.py
Files to create:
watcher_dashboard/test/unit/__init__.pywatcher_dashboard/test/local_fixtures/__init__.pywatcher_dashboard/test/local_fixtures/config.pywatcher_dashboard/test/playwright/__init__.py
Files to modify:
- All existing test files (update imports if needed)
tox.ini(update test discovery paths)test-requirements.txt(add fixtures library if not present)setup.cfgorpyproject.toml(update test paths if configured)
Implementation:
Step 1: Create new directory structure
mkdir -p watcher_dashboard/test/unit
mkdir -p watcher_dashboard/test/local_fixtures
mkdir -p watcher_dashboard/test/playwright
touch watcher_dashboard/test/unit/__init__.py
touch watcher_dashboard/test/local_fixtures/__init__.py
touch watcher_dashboard/test/playwright/__init__.pyStep 2: Move existing tests to unit/
# Move test files to unit/ directory
mv watcher_dashboard/test/test_*.py watcher_dashboard/test/unit/ 2>/dev/null || true
mv watcher_dashboard/test/*_tests/ watcher_dashboard/test/unit/ 2>/dev/null || true
# Keep settings.py at top level (needed by tox/Django)Step 3: Create initial config fixtures module
File: watcher_dashboard/test/local_fixtures/config.py
# 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
#
# http://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.
"""Test fixtures for configuration testing.
This module provides reusable fixtures for testing configuration-related
functionality, particularly clearing memoized caches between tests.
Usage:
from watcher_dashboard.test.local_fixtures import config as config_fixtures
class MyConfigTest(TestCase):
def setUp(self):
super().setUp()
self.useFixture(config_fixtures.ConfigMemoizedCache())
"""
import fixtures
class ConfigMemoizedCache(fixtures.Fixture):
"""Fixture to clear memoized config caches before and after tests.
The watcher_dashboard.config module uses @memoized.memoized decorator
for performance. This fixture ensures caches are cleared between tests
to prevent test pollution.
Example:
class ConfigTest(TestCase):
def setUp(self):
super().setUp()
self.useFixture(ConfigMemoizedCache())
def test_get_policy_files(self):
# Cache is clean for this test
result = config.get_policy_files()
# ...
"""
def _clear_caches(self):
"""Clear all memoized caches in config module."""
# Import here to avoid circular imports during test discovery
from watcher_dashboard import config
# Clear each memoized function's cache
for attr_name in dir(config):
attr = getattr(config, attr_name)
if hasattr(attr, 'cache_clear'):
attr.cache_clear()
def setUp(self):
"""Clear caches before test starts."""
super().setUp()
self._clear_caches()
# Register cleanup to clear after test completes
self.addCleanup(self._clear_caches)File: watcher_dashboard/test/local_fixtures/__init__.py
# 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
#
# http://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.
"""Local test fixtures for watcher-dashboard tests.
This package provides reusable test fixtures following the pattern
established in the main Watcher project.
Available fixture modules:
- config: Configuration and memoization fixtures
"""Step 4: Ensure fixtures library is available
File: test-requirements.txt
# Add if not already present
fixtures>=3.0.0
Step 5: Update tox.ini test discovery
File: tox.ini
[testenv:py3]
commands =
# Update to discover tests in new location
python manage.py test \
--settings=watcher_dashboard.test.settings \
--exclude-tag integration \
watcher_dashboard.test.unitTesting:
# Verify all tests still discoverable and passing
python manage.py test \
--settings=watcher_dashboard.test.settings \
--exclude-tag integration \
watcher_dashboard.test.unit
# Run via tox
tox -e py3
# Verify imports work
python -c "from watcher_dashboard.test.local_fixtures import config"Commit Message:
[Phase 0.2] Restructure test directory for fixture pattern
Problem:
- Tests scattered in flat structure
- No infrastructure for reusable fixtures
- setUp/tearDown used for common patterns like cache clearing
- Want consistency with main Watcher project test structure
Solution:
- Move existing tests to test/unit/ subdirectory
- Create test/local_fixtures/ for reusable fixtures
- Create test/playwright/ placeholder for future
- Implement ConfigMemoizedCache fixture for config testing
- Update test discovery in tox.ini
New structure:
watcher_dashboard/test/
├── unit/ # All existing unit tests
├── local_fixtures/ # Reusable fixtures (config.py created)
└── playwright/ # Placeholder for future browser tests
Files created:
- test/unit/__init__.py
- test/local_fixtures/__init__.py
- test/local_fixtures/config.py (ConfigMemoizedCache fixture)
- test/playwright/__init__.py
Files modified:
- tox.ini (updated test discovery path)
- test-requirements.txt (added fixtures>=3.0.0 if needed)
- All test files moved to unit/ subdirectory
Testing:
- All existing tests pass in new location
- tox -e py3 passes
- Fixture module imports successfully
- ConfigMemoizedCache fixture works correctly
Benefits:
- Consistent with main Watcher project pattern
- Reusable fixtures replace setUp/tearDown boilerplate
- Clear separation of test types (even if only unit tests used)
- Foundation for future fixture additions
References:
- Watcher project test structure
- Python fixtures library pattern
Partial-Implements: code-quality-improvement
Acceptance Criteria:
- All existing tests moved to
test/unit/ test/local_fixtures/created with config.py fixturetest/playwright/created (placeholder)- All tests still pass
- tox test discovery updated
- Fixture pattern documented
Goal: Eliminate direct symbol imports in favor of module-level imports throughout the codebase.
Duration: 1-2 days
Current State (from analysis):
- Star imports: 2 files (only test/settings.py is allowed)
- Relative imports: 0 (already clean! ✓)
- Direct symbol imports: ~4 files need refactoring (audit_templates/views.py, audits/views.py, etc.)
Scope: Convert all direct symbol imports to module-level imports across the codebase.
Analysis Approach: The commit author should:
- Use grep to find symbol imports:
grep -rn "from .* import [A-Z]" watcher_dashboard/ --include="*.py" | grep -v test/settings.py - Review each file to distinguish submodule imports (acceptable) from symbol imports (needs fixing)
- Manually review and refactor each affected file
Expected Files to Modify (based on initial analysis):
watcher_dashboard/api/watcher.pywatcher_dashboard/common/client.pywatcher_dashboard/content/audit_templates/views.pywatcher_dashboard/content/audit_templates/forms.pywatcher_dashboard/content/audits/views.pywatcher_dashboard/content/audits/forms.py- Other content module files with symbol imports
watcher_dashboard/utils/files as needed
Refactoring Patterns:
Pattern 1: Django utilities
# Before
from django.utils.translation import gettext_lazy as _
from django.conf import settings
# After
from django.utils import translation
from django.conf import settings # Module import - acceptable
_T = translation.gettext_lazy
# Usage updates
error_msg = _("Error") # Before
error_msg = _T("Error") # AfterPattern 2: Horizon framework
# Before
from horizon import exceptions, forms, tables
from openstack_dashboard.api import base
# After
from horizon import exceptions
from horizon import forms
from horizon import tables
from openstack_dashboard import api as os_api
# Usage updates
class MyForm(forms.SelfHandlingForm): # Before
class MyForm(forms.SelfHandlingForm): # After (no change - forms is a module)
raise exceptions.NotFound() # Before
raise exceptions.NotFound() # After (no change - exceptions is a module)
class Audit(base.APIDictWrapper): # Before
class Audit(os_api.base.APIDictWrapper): # AfterPattern 3: Internal project imports
# Before
from watcher_dashboard.api import watcher
from watcher_dashboard.content.audits import forms as wforms
# After
from watcher_dashboard import api as watcher_api # If accessing watcher.Audit
from watcher_dashboard.content.audits import forms as audit_forms # Submodule - acceptable
# Usage updates
watcher.Audit.list(request) # Before
watcher_api.watcher.Audit.list(request) # After
wforms.CreateForm() # Before
audit_forms.CreateForm() # After (submodule, so less change needed)Acceptable Import Clarification:
# ACCEPTABLE: Submodule imports
from watcher_dashboard.content import audits # audits is a submodule
from watcher_dashboard.content.audits import forms as audit_forms # forms is a submodule
from watcher_dashboard import api as watcher_api # api is a submodule
# NOT ACCEPTABLE: Symbol imports (classes, functions, constants)
from watcher_dashboard.content.audits.forms import CreateAuditForm # CreateAuditForm is a class
from horizon.exceptions import NotFound # NotFound is a class
from django.utils.translation import gettext_lazy # gettext_lazy is a functionTasks:
- Discovery: Grep for all symbol imports, manually classify each
- Refactor imports: Update import statements in each file
- Update call sites: Search and replace all usage sites
- Test incrementally: Run tests after each module to catch issues early
- Add CI enforcement: Create tox environment and pre-commit hook
- Update docstrings: Improve business logic explanations where needed
Testing:
# Test each module as you refactor it
python manage.py test watcher_dashboard.api
python manage.py test watcher_dashboard.common
python manage.py test watcher_dashboard.content.audit_templates
python manage.py test watcher_dashboard.content.audits
# ... etc for each module
# Full test suite at the end
tox -e py3
# Verify no regressions in style checks
tox -e pep8
# Manual verification: search for remaining symbol imports
grep -rn "from django.utils.translation import gettext_lazy" watcher_dashboard/ --include="*.py"
# Should return no results
grep -rn "from horizon.exceptions import" watcher_dashboard/ --include="*.py"
# Should return no results (horizon.exceptions is a module, so this is OK if found)CI Enforcement (add as part of this commit):
File: tox.ini
[testenv:import-policy]
description = Enforce import policy standards
skip_install = false
commands =
# Verify no star imports except test/settings.py
bash -c '! grep -rn "from .* import \*" watcher_dashboard --include="*.py" | grep -v test/settings.py'
# Verify no relative imports
bash -c '! grep -rn "from \\.\\+ import" watcher_dashboard --include="*.py"'
# Additional checks can be added as needed
[testenv]
# Add to default test env list if desiredFile: .pre-commit-config.yaml (add to existing repos section)
- repo: local
hooks:
- id: check-import-policy
name: Check import policy compliance
entry: bash -c '! grep -rn "from .* import \*" watcher_dashboard --include="*.py" | grep -v test/settings.py'
language: system
types: [python]
pass_filenames: falseDocumentation (inline in modified files):
No inline comments about import patterns are needed—style guidelines belong in contributor documentation (doc/source/contributor/code_patterns.rst), not in code.
Code comments should explain business logic and intent. Example for api/watcher.py:
import logging
from django.conf import settings
from django.utils import translation
from openstack_dashboard import api as os_api
import watcher_dashboard.config as watcher_config
from watcher_dashboard.common import client as wv_client
from watcher_dashboard import utils
LOG = logging.getLogger(__name__)
WATCHER_SERVICE: str = 'infra-optim'
def watcherclient(request, api_version=None):
"""Get Watcher API client.
Establishes connection to the Watcher optimization service using
the user's token and the configured service endpoint. Defaults to
minimum supported microversion (1.0) unless explicitly overridden.
:param request: HTTP request with user authentication
:param api_version: API microversion (defaults to minimum)
:returns: Configured watcherclient instance
"""
# Policy file must be registered before first API call
watcher_config.register_watcher_policy()
endpoint = os_api.base.url_for(request, WATCHER_SERVICE)
# ... rest of implementationCommit Message Template:
[Phase 1] Refactor all imports to use module-level imports
Problem:
- Direct symbol imports (from X import Y where Y is a class/function)
hinder static analysis and make dependencies unclear
- Current codebase has ~4 files with symbol imports
- Violates import policy for maintainable code
Solution:
- Convert all symbol imports to module-level imports
- Update call sites to use module.symbol access pattern
- Preserve submodule imports (from X import module) as acceptable
- Add CI enforcement via tox and pre-commit
- Improve docstrings to explain business logic
Files modified:
- watcher_dashboard/api/watcher.py
- watcher_dashboard/common/client.py
- watcher_dashboard/content/*/views.py
- watcher_dashboard/content/*/forms.py
- tox.ini (added import-policy environment)
- .pre-commit-config.yaml (added import check hook)
Testing:
- All module tests pass
- tox -e py3 passes
- tox -e pep8 passes
- tox -e import-policy passes (new)
- Manual verification of import patterns
Documentation:
- Import policy enforced in CI
- Code comments explain business logic, not style
- See doc/source/contributor/code_patterns.rst (Phase 4)
Partial-Implements: code-quality-improvement
Acceptance Criteria:
- Zero direct symbol imports (except TYPE_CHECKING blocks if any)
- Zero relative imports
- Only one file with star imports (test/settings.py)
- All tests pass
- CI enforcement added and passing
- Docstrings explain business logic where needed
Alternative Approach (if many files affected):
If the analysis reveals >10 files need changes, consider splitting into:
- Commit 1.1a: Refactor api/ and common/ modules
- Commit 1.1b: Refactor content/audit_templates/ and content/audits/
- Commit 1.1c: Refactor remaining content/ modules
- Commit 1.1d: Add CI enforcement and verification
Each commit should still be independently testable and include its portion of documentation updates.
Goal: Eliminate unnecessary use of getattr/hasattr/setattr where attribute names are static and known at development time.
Duration: 1-2 days
Current State (from analysis):
- getattr usage: 11 files
- hasattr usage: 4 files
- setattr usage: 2 files
Strategy: Remove static reflection helpers while preserving legitimate dynamic usage (API version detection, plugin systems). Django settings access will be refactored in Phase 3.
Scope: Replace getattr/hasattr/setattr with direct attribute access where the attribute name is static and known.
Analysis Approach: The commit author should:
- Grep for reflection helpers:
grep -rn "getattr(" watcher_dashboard/ --include="*.py" | grep -v test/ grep -rn "hasattr(" watcher_dashboard/ --include="*.py" | grep -v test/ grep -rn "setattr(" watcher_dashboard/ --include="*.py" | grep -v test/
- Manually review each usage and classify:
- Removable: Static attribute name known at development time
- Django settings: Keep temporarily, mark with TODO for Phase 3
- Dynamic: Truly dynamic (API version detection, etc.) - keep with comment
Expected Files to Modify:
watcher_dashboard/api/watcher.py(getattr/setattr for settings - mark TODO)watcher_dashboard/common/client.py(getattr usage)watcher_dashboard/content/audit_templates/forms.py(hasattr usage)watcher_dashboard/content/audits/views.py(hasattr usage)watcher_dashboard/content/action_plans/tables.py(getattr usage)watcher_dashboard/content/actions/tables.py(getattr usage)watcher_dashboard/utils/utils.py(getattr usage)- Other content module files
Refactoring Patterns:
Pattern 1: Static attribute access
# Before: getattr with literal attribute name
name = getattr(user, 'username')
value = getattr(obj, 'status', 'unknown')
# After: Direct attribute access
name = user.username
value = obj.status if hasattr(obj, 'status') else 'unknown'
# OR even better if status should always exist:
value = obj.status # Let AttributeError propagate if missingPattern 2: hasattr for flow control
# Before: hasattr for known attribute
if hasattr(audit, 'state'):
current_state = audit.state
else:
current_state = None
# After: Direct access with getattr only if truly optional
# If 'state' should always exist per interface:
current_state = audit.state
# If 'state' is genuinely optional:
current_state = getattr(audit, 'state', None) # Still needed for optionalPattern 3: Django settings access (temporary)
# Before
def insert_watcher_policy_file():
policy_files = getattr(settings, 'POLICY_FILES', {})
policy_files['infra-optim'] = 'watcher_policy.json'
setattr(settings, 'POLICY_FILES', policy_files)
# After (Phase 2 - temporary with TODO)
def insert_watcher_policy_file():
"""Register Watcher policy file with Django settings.
TODO(Phase 3): Migrate to watcher_dashboard.config module.
This function uses getattr/setattr for Django settings access,
which will be replaced with centralized config module in Phase 3.
"""
# TODO(Phase 3): Replace with config module
policy_files = getattr(settings, 'POLICY_FILES', {})
policy_files['infra-optim'] = 'watcher_policy.json'
setattr(settings, 'POLICY_FILES', policy_files)Pattern 4: API version detection (keep - truly dynamic)
# Before & After (keep as-is, optionally add comment explaining why)
# API capability varies by microversion - use feature detection
if hasattr(client, 'audit_create_with_start_end'):
# Microversion 1.1+ supports start/end time parameters
result = client.audit_create_with_start_end(params)
else:
# Fall back to basic audit creation for older API versions
result = client.audit_create(params)Tasks:
- Discovery & Classification: Grep and manually classify all reflection usage
- Refactor Static Cases: Replace static getattr/hasattr/setattr with direct access
- Mark Settings Access: Add TODO(Phase 3) comments to Django settings access
- Document Dynamic Cases: Add comments explaining why dynamic behavior is required
- Test: Run tests after each file/module
- Add CI Enforcement: Create verification in tox
- Update Docstrings: Improve business logic explanations where needed
Testing:
# Test each module as you refactor
python manage.py test watcher_dashboard.api
python manage.py test watcher_dashboard.common
python manage.py test watcher_dashboard.content.audit_templates
python manage.py test watcher_dashboard.content.audits
# ... etc
# Full test suite
tox -e py3
# Verify no regressions
tox -e pep8
# Manual verification: find remaining reflection usage
grep -rn "getattr(" watcher_dashboard/ --include="*.py" | grep -v "test/" | grep -v "TODO(Phase 3)"
# Should only show truly dynamic cases or settings access with TODOCI Enforcement (add to tox.ini):
[testenv:reflection-policy]
description = Verify reflection helper usage policy
skip_install = false
commands =
# Verify all getattr(settings, ...) have TODO(Phase 3) marker
bash -c 'for file in $(grep -rl "getattr(settings" watcher_dashboard --include="*.py" | grep -v test/ | grep -v config.py); do grep -q "TODO(Phase 3)" "$file" || (echo "Missing TODO(Phase 3) in $file" && exit 1); done'
# Additional reflection policy checks
python -c "import sys; print('Reflection policy check: PASS')"Documentation:
No inline comments about reflection helper policy are needed—style guidelines belong in contributor documentation (doc/source/contributor/code_quality.rst), not in code.
Code comments should explain business logic and intent. Example for content/audits/views.py:
# watcher_dashboard/content/audits/views.py
def get_data(self):
"""Retrieve all audits for display in the audits table.
Fetches audit list from Watcher API and normalizes display values.
Missing names are replaced with '-' for consistent UI presentation.
"""
audits = watcher_api.watcher.Audit.list(self.request)
# Normalize display values - some audits may lack template/goal names
for audit in audits:
audit.audit_template_name = audit.audit_template_name or '-'
audit.goal_name = audit.goal_name or '-'
return auditsCommit Message Template:
[Phase 2] Remove static reflection helpers (getattr/hasattr/setattr)
Problem:
- Reflection helpers (getattr/hasattr/setattr) used for static attributes
- Hinders static analysis and type checking
- Makes code harder to understand and maintain
- Found in 11 files (getattr), 4 files (hasattr), 2 files (setattr)
Solution:
- Replace static attribute access with direct member access
- Mark Django settings access with TODO(Phase 3) for config module migration
- Preserve truly dynamic reflection (API version detection) with comments
- Add CI verification to prevent regressions
Files modified:
- watcher_dashboard/api/watcher.py (marked settings access for Phase 3)
- watcher_dashboard/common/client.py (removed static getattr)
- watcher_dashboard/content/*/forms.py (removed static hasattr)
- watcher_dashboard/content/*/views.py (removed static getattr/hasattr)
- watcher_dashboard/content/*/tables.py (removed static getattr)
- tox.ini (added reflection-policy environment)
Patterns addressed:
- Static getattr → direct attribute access
- Static hasattr → direct access or getattr for truly optional
- Settings access → marked with TODO(Phase 3)
- Dynamic detection → preserved with business logic comments
Testing:
- All module tests pass
- tox -e py3 passes
- tox -e pep8 passes
- tox -e reflection-policy passes (new)
- Manual verification of reflection usage
Documentation:
- Code comments explain business logic where appropriate
- TODO(Phase 3) markers for settings migration
- See doc/source/contributor/code_patterns.rst (Phase 4)
Next Steps:
- Phase 3 will create watcher_dashboard.config module
- All TODO(Phase 3) markers will be addressed
Partial-Implements: code-quality-improvement
Acceptance Criteria:
- All static reflection helpers removed
- Django settings access marked with TODO(Phase 3)
- Truly dynamic reflection preserved with business logic comments
- All tests pass
- CI enforcement added and passing
- Clear understanding of remaining reflection usage (why it's needed)
Goal: Create watcher_dashboard/config.py module to provide typed, validated
access to Django settings, eliminating all remaining getattr(settings, ...) usage
outside the config module.
Duration: 2-3 days
Current State (after Phase 2):
- All getattr(settings, ...) calls marked with TODO(Phase 3)
- Primarily in api/watcher.py for policy file registration
Scope: Create centralized config module, add comprehensive tests, migrate all settings access, and update documentation.
Files to create:
watcher_dashboard/config.py- Centralized configuration modulewatcher_dashboard/test/test_config.py- Comprehensive unit tests
Files to modify:
watcher_dashboard/api/watcher.py- Use config module- Any other files with getattr(settings, ...) - Use config module
Implementation:
File: watcher_dashboard/config.py
# 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
#
# http://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.
"""Centralized configuration access for Watcher Dashboard.
This module provides typed, validated access to Django settings, eliminating
the need for getattr() reflection helpers throughout the codebase.
Pattern inspired by manila-ui (openstack/manila-ui/manila_ui/features.py)
but enhanced with:
- Type hints for all return values (Python 3.10+)
- Validation before returning values
- Comprehensive docstrings
Usage Examples:
>>> import watcher_dashboard.config as watcher_config
>>> watcher_config.register_watcher_policy()
>>> policy_file = watcher_config.get_watcher_policy_file()
References:
- Manila-UI features.py:
https://opendev.org/openstack/manila-ui/src/branch/master/manila_ui/features.py
- Code quality guide: doc/source/contributor/code_quality.rst
"""
from django.conf import settings
from horizon.utils import memoized
@memoized.memoized
def get_policy_files() -> dict[str, str]:
"""Get the policy files configuration.
Returns the POLICY_FILES setting which maps service names to
policy file names.
:returns: Dictionary mapping service names to policy file names
:raises TypeError: If POLICY_FILES is not a dictionary
Example:
>>> policy_files = get_policy_files()
>>> watcher_policy = policy_files.get('infra-optim', 'default.json')
"""
policy_files = getattr(settings, 'POLICY_FILES', {})
if not isinstance(policy_files, dict):
raise TypeError(
f"POLICY_FILES must be a dict, got {type(policy_files)}"
)
return policy_files
@memoized.memoized
def get_watcher_policy_file() -> str:
"""Get the Watcher policy file name.
:returns: Policy file name for Watcher service
:rtype: str
Example:
>>> filename = get_watcher_policy_file()
>>> print(filename)
'watcher_policy.json'
"""
policy_files = get_policy_files()
return policy_files.get('infra-optim', 'watcher_policy.json')
def register_watcher_policy() -> None:
"""Register Watcher policy file with Django settings.
This function should be called during application initialization.
It modifies the Django settings to register Watcher's policy file.
Note: This is one of the few places where settings mutation is
necessary. This function encapsulates that complexity so the rest
of the codebase doesn't need reflection helpers.
Example:
>>> # Called during application startup
>>> register_watcher_policy()
"""
policy_files = getattr(settings, 'POLICY_FILES', {})
policy_files['infra-optim'] = 'watcher_policy.json'
settings.POLICY_FILES = policy_filesFile: watcher_dashboard/test/unit/test_config.py
# 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
#
# http://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.
"""Tests for watcher_dashboard.config module."""
from django.conf import settings
from django.test import TestCase
from unittest import mock
from watcher_dashboard import config
from watcher_dashboard.test.local_fixtures import config as config_fixtures
class ConfigPolicyFilesTests(TestCase):
"""Tests for policy file configuration functions."""
def setUp(self):
"""Set up test fixtures."""
super().setUp()
# Use fixture to manage memoized cache clearing
self.useFixture(config_fixtures.ConfigMemoizedCache())
def test_get_policy_files_returns_dict(self):
"""Verify get_policy_files returns dictionary."""
result = config.get_policy_files()
self.assertIsInstance(result, dict)
def test_get_policy_files_with_custom_setting(self):
"""Test get_policy_files with custom POLICY_FILES."""
custom = {'service': 'policy.json'}
with mock.patch.object(settings, 'POLICY_FILES', custom):
# Fixture handles cache clearing
result = config.get_policy_files()
self.assertEqual(result, custom)
def test_get_policy_files_invalid_type_raises(self):
"""Verify get_policy_files raises TypeError for non-dict."""
with mock.patch.object(settings, 'POLICY_FILES', 'invalid'):
# Fixture handles cache clearing
with self.assertRaises(TypeError) as cm:
config.get_policy_files()
self.assertIn('must be a dict', str(cm.exception))
def test_get_watcher_policy_file_default(self):
"""Test get_watcher_policy_file returns default."""
with mock.patch.object(settings, 'POLICY_FILES', {}):
# Fixture handles cache clearing
result = config.get_watcher_policy_file()
self.assertEqual(result, 'watcher_policy.json')
def test_get_watcher_policy_file_custom(self):
"""Test get_watcher_policy_file with custom value."""
custom = {'infra-optim': 'custom_watcher.json'}
with mock.patch.object(settings, 'POLICY_FILES', custom):
# Fixture handles cache clearing
result = config.get_watcher_policy_file()
self.assertEqual(result, 'custom_watcher.json')
def test_register_watcher_policy_creates_entry(self):
"""Verify register_watcher_policy adds policy file."""
# Create a fresh dict to avoid modifying global settings
test_policy_files = {}
with mock.patch.object(settings, 'POLICY_FILES', test_policy_files):
config.register_watcher_policy()
self.assertIn('infra-optim', settings.POLICY_FILES)
self.assertEqual(
settings.POLICY_FILES['infra-optim'],
'watcher_policy.json'
)
def test_register_watcher_policy_preserves_existing(self):
"""Verify register_watcher_policy preserves other entries."""
test_policy_files = {'other-service': 'other.json'}
with mock.patch.object(settings, 'POLICY_FILES', test_policy_files):
config.register_watcher_policy()
self.assertIn('other-service', settings.POLICY_FILES)
self.assertIn('infra-optim', settings.POLICY_FILES)
self.assertEqual(
settings.POLICY_FILES['other-service'],
'other.json'
)Migration Example (watcher_dashboard/api/watcher.py):
Before (Phase 2 state):
def insert_watcher_policy_file():
"""Register Watcher policy file with Django settings.
TODO(Phase 3): Migrate to watcher_dashboard.config module.
"""
# TODO(Phase 3): Replace with config module
policy_files = getattr(settings, 'POLICY_FILES', {})
policy_files['infra-optim'] = 'watcher_policy.json'
setattr(settings, 'POLICY_FILES', policy_files)After (Phase 3):
import watcher_dashboard.config as watcher_config
def insert_watcher_policy_file():
"""Register Watcher policy file with Django settings.
Delegates to centralized config module for settings management.
See watcher_dashboard/config.py for implementation details.
"""
watcher_config.register_watcher_policy()Tasks:
- Create
watcher_dashboard/config.pywith type hints and validation - Create comprehensive unit tests in
test/unit/test_config.pyusing ConfigMemoizedCache fixture (from Phase 0.2) - Migrate
api/watcher.pyand any other files to use config module - Remove all TODO(Phase 3) markers
- Run tests to verify behavior is unchanged
- Verify no getattr(settings, ...) remains outside config.py
Testing:
# Test config module
python manage.py test watcher_dashboard.test.unit.test_config
# Verify fixture works correctly
python -c "from watcher_dashboard.test.local_fixtures.config import ConfigMemoizedCache; print('Fixture imported successfully')"
# Verify coverage of config module
coverage run --source=watcher_dashboard.config manage.py test watcher_dashboard.test.unit.test_config
coverage report -m
# Should show >95% coverage
# Test modules that were migrated
python manage.py test watcher_dashboard.test.unit
# Full test suite
tox -e py3
# Verify no direct settings access remains
grep -rn "getattr(settings" watcher_dashboard/ --include="*.py" | \
grep -v "config.py" | grep -v "test/"
# Should return no results
# Style checks
tox -e pep8Commit Message Template:
[Phase 3] Create centralized config module for typed settings access
Problem:
- Django settings accessed via getattr throughout codebase
- Hinders static analysis and type checking
- No centralized validation of settings values
- Defaults scattered across multiple files
- After Phase 2, all settings access marked with TODO(Phase 3)
Solution:
- Create watcher_dashboard/config.py for centralized settings access
- Provide typed, validated functions for each setting
- Use memoization for performance (horizon.utils.memoized)
- Migrate all getattr(settings, ...) to use config module
- Pattern inspired by manila-ui but enhanced with type hints
Files created:
- watcher_dashboard/config.py (typed config functions)
- watcher_dashboard/test/unit/test_config.py (>95% coverage, uses fixture pattern)
Files modified:
- watcher_dashboard/api/watcher.py (use config.register_watcher_policy())
- [any other files with settings access]
Implementation details:
- Type hints using Python 3.10+ syntax (str | None, dict[str, str])
- Validation with TypeError/ValueError for invalid settings
- Comprehensive docstrings with usage examples
- Memoization for efficient repeated access
Testing:
- test_config.py covers all functions with >95% coverage
- Tests use ConfigMemoizedCache fixture (Phase 0.2 pattern)
- All existing tests pass unchanged (behavior preserved)
- tox -e py3 passes
- Manual verification: no getattr(settings) outside config.py
Documentation:
- Inline docstrings with examples in config.py
- References Manila-UI pattern
- See doc/source/contributor/code_quality.rst (Phase 4)
Benefits:
- Static analysis enabled (ready for mypy in Phase 5)
- Centralized defaults and validation
- Improved testability (easy to mock config functions)
- Better discoverability (all settings in one module)
- Serves as example for future config additions
References:
- Manila-UI features.py pattern
- OpenStack Horizon best practices
Implements: code-quality-config-module
Partial-Implements: code-quality-improvement
Acceptance Criteria:
watcher_dashboard/config.pycreated with type hints- Comprehensive unit tests with >95% coverage
- All Django settings access migrated to config module
- No getattr(settings, ...) outside config.py (except tests)
- All TODO(Phase 3) markers removed
- All tests pass
- No behavior changes
Goal: Create comprehensive RST documentation encoding code quality policies, patterns, and best practices.
Duration: 1 day
Scope: Create complete code quality and code patterns documentation in RST format.
Files to create:
doc/source/contributor/code_quality.rst- Standards and policiesdoc/source/contributor/code_patterns.rst- Examples and patterns
Files to modify:
doc/source/contributor/index.rst- Add new docs to toctreeREADME.rst- Link to code quality guideCONTRIBUTING.rst- Reference standards
File: doc/source/contributor/code_quality.rst
See Appendix A for complete content (abbreviated here for space).
Key sections:
- Overview (static analysis, explicit dependencies, type safety)
- Import Standards (module-level, no star, no relative, TYPE_CHECKING)
- Configuration Access (config module pattern, benefits)
- Reflection Helpers (when to avoid, when acceptable)
- Testing Standards (requirements, CI enforcement)
- Type Hints (optional, guidelines)
- Code Review Checklist
- Quick Reference (common commands)
File: doc/source/contributor/code_patterns.rst
==============
Code Patterns
==============
This document provides concrete examples of code patterns used in Watcher Dashboard.
For policy explanations, see :doc:`code_quality`.
Import Patterns
===============
Pattern 1: Django Framework
----------------------------
.. code-block:: python
# Standard library
import logging
# Django
from django.conf import settings
from django.utils import translation
# Create module-level aliases for commonly used functions
_T = translation.gettext_lazy
# Usage
LOG = logging.getLogger(__name__)
error_msg = _T("An error occurred")
Pattern 2: Horizon Framework
-----------------------------
.. code-block:: python
# Horizon imports - import modules, not symbols
from horizon import exceptions
from horizon import forms
from horizon import tables
from openstack_dashboard import api as os_api
# Usage examples
class CreateAuditForm(forms.SelfHandlingForm):
"""Form for creating audits."""
pass
def my_view(request):
try:
audit = os_api.base.APIDictWrapper(data)
except Exception as e:
raise exceptions.NotFound()
Pattern 3: Internal Project Imports
------------------------------------
.. code-block:: python
# Internal imports - prefer module-level
import watcher_dashboard.config as watcher_config
from watcher_dashboard import api as watcher_api
from watcher_dashboard.common import client as wv_client
from watcher_dashboard.content.audits import forms as audit_forms
# Usage
def create_audit(request, template_uuid):
watcher_config.register_watcher_policy()
client = wv_client.get_client(request)
audit = watcher_api.watcher.Audit.create(
request,
template_uuid,
'oneshot'
)
return audit
Pattern 4: TYPE_CHECKING Imports
---------------------------------
When adding type hints, use TYPE_CHECKING to avoid runtime import cycles:
.. code-block:: python
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# These imports only used for type checking, not at runtime
from django.http import HttpRequest
from watcher_dashboard.api.watcher import Audit
# Runtime imports
from watcher_dashboard import api as watcher_api
def process_audit(request: HttpRequest, audit_id: str) -> "Audit":
"""Process an audit by ID."""
return watcher_api.watcher.Audit.get(request, audit_id)
Configuration Access Patterns
==============================
Pattern 1: Reading Configuration
---------------------------------
.. code-block:: python
import watcher_dashboard.config as watcher_config
def my_view(request):
# Get policy file name
policy_file = watcher_config.get_watcher_policy_file()
# Get policy files dict
all_policies = watcher_config.get_policy_files()
Pattern 2: Adding New Settings
-------------------------------
When adding a new Django setting, add a function to config.py:
.. code-block:: python
# In watcher_dashboard/config.py
@memoized.memoized
def get_audit_retention_days() -> int:
"""Get number of days to retain completed audits.
:returns: Retention period in days
:raises ValueError: If value is not positive integer
"""
days = getattr(settings, 'WATCHER_AUDIT_RETENTION_DAYS', 90)
if not isinstance(days, int) or days <= 0:
raise ValueError(
f"WATCHER_AUDIT_RETENTION_DAYS must be positive, got {days}"
)
return days
Then use in application code:
.. code-block:: python
import watcher_dashboard.config as watcher_config
def cleanup_old_audits():
retention_days = watcher_config.get_audit_retention_days()
cutoff_date = timezone.now() - timedelta(days=retention_days)
# ... cleanup logic
Reflection Helper Patterns
===========================
Pattern 1: Direct Attribute Access (Preferred)
-----------------------------------------------
.. code-block:: python
# Preferred: Direct access
def get_audit_info(audit):
return {
'uuid': audit.uuid,
'name': audit.name,
'state': audit.state,
}
# Avoid: getattr for static attributes
def get_audit_info_bad(audit):
return {
'uuid': getattr(audit, 'uuid'), # Unnecessary
'name': getattr(audit, 'name'), # Unnecessary
'state': getattr(audit, 'state'), # Unnecessary
}
Pattern 2: Optional Attributes
-------------------------------
.. code-block:: python
# For truly optional attributes, getattr with default is OK
def format_audit(audit):
description = getattr(audit, 'description', None)
if description:
return f"{audit.name}: {description}"
return audit.name
Pattern 3: API Version Detection (Acceptable)
----------------------------------------------
.. code-block:: python
# OK: Method availability depends on API microversion
def create_audit_with_time(client, params):
# Check if API supports start/end time (microversion 1.1+)
if hasattr(client, 'audit_create_with_start_end'):
return client.audit_create_with_start_end(params)
else:
# Fall back to basic create
return client.audit_create(params)
Testing Patterns
================
Pattern 1: Using Fixtures for Test Setup
-----------------------------------------
The project uses the ``fixtures`` library for reusable test setup, following
the pattern established in the main Watcher project.
.. code-block:: python
from django.test import TestCase
from watcher_dashboard.test.local_fixtures import config as config_fixtures
class ConfigTest(TestCase):
def setUp(self):
super().setUp()
# Use fixture to manage memoized cache clearing
self.useFixture(config_fixtures.ConfigMemoizedCache())
def test_get_policy_files(self):
# Cache is automatically cleared before and after this test
result = config.get_policy_files()
self.assertIsInstance(result, dict)
Benefits of fixtures over setUp/tearDown:
- Automatic cleanup via ``addCleanup()``
- Reusable across test classes
- Composable (can use multiple fixtures)
- Clear intent (fixture name describes what it provides)
Pattern 2: Config Module Mocking
---------------------------------
.. code-block:: python
from unittest import mock
from django.test import TestCase
from watcher_dashboard import config
from watcher_dashboard.test.local_fixtures import config as config_fixtures
class MyTest(TestCase):
def setUp(self):
super().setUp()
# Fixture ensures clean cache state
self.useFixture(config_fixtures.ConfigMemoizedCache())
def test_with_custom_config(self):
# Mock config function
with mock.patch.object(
config,
'get_watcher_policy_file',
return_value='custom.json'
):
result = my_function()
# Test with custom policy file
Pattern 3: API Client Mocking
------------------------------
.. code-block:: python
from unittest import mock
from watcher_dashboard import api as watcher_api
class AuditViewTest(TestCase):
@mock.patch.object(watcher_api.watcher, 'Audit')
def test_create_audit(self, mock_audit):
mock_audit.create.return_value = fake_audit
response = self.client.post(url, data)
self.assertEqual(response.status_code, 200)
mock_audit.create.assert_called_once()
Type Hint Patterns
==================
Pattern 1: Function Signatures
-------------------------------
.. code-block:: python
from __future__ import annotations
def create_audit(
request: HttpRequest,
template_uuid: str,
audit_type: str,
name: str | None = None,
parameters: dict[str, Any] | None = None,
) -> Audit:
"""Create a new audit."""
# Implementation
Pattern 2: Class Attributes
----------------------------
.. code-block:: python
from __future__ import annotations
class AuditManager:
"""Manager for audit operations."""
_client: Client
_cache: dict[str, Audit]
_timeout: int = 30
def __init__(self, client: Client) -> None:
self._client = client
self._cache = {}
Common Anti-Patterns
====================
Anti-Pattern 1: Mixing Import Styles
-------------------------------------
.. code-block:: python
# Bad: Mixing symbol and module imports
from django.utils.translation import gettext_lazy as _
from django.utils import timezone # Mixed styles!
# Good: Consistent module imports
from django.utils import translation
from django.utils import timezone
_T = translation.gettext_lazy
Anti-Pattern 2: Circular Dependencies from Symbol Imports
----------------------------------------------------------
.. code-block:: python
# Bad: Can cause circular import issues
# In module_a.py
from module_b import ClassB
# In module_b.py
from module_a import ClassA # Circular!
# Good: Import modules, not symbols
# In module_a.py
import module_b
# In module_b.py
import module_a
Anti-Pattern 3: Nested getattr Calls
-------------------------------------
.. code-block:: python
# Bad: Nested getattr
value = getattr(getattr(obj, 'config', {}), 'setting', None)
# Good: Direct access with try/except if needed
try:
value = obj.config.setting
except AttributeError:
value = None
# Or: Safe navigation if config might not exist
config = getattr(obj, 'config', {})
value = config.get('setting')
References
==========
- :doc:`code_quality` - Code quality policies and standards
- :doc:`/index` - Main documentation index
- `fixtures library <https://pypi.org/project/fixtures/>`_ - Test fixture pattern
- `Manila-UI features.py <https://opendev.org/openstack/manila-ui/src/branch/master/manila_ui/features.py>`_
- `PEP 484 <https://peps.python.org/pep-0484/>`_ - Type Hints
- `PEP 585 <https://peps.python.org/pep-0585/>`_ - Type Hinting GenericsUpdate doc/source/contributor/index.rst:
.. toctree::
:maxdepth: 2
contributing
code_quality
code_patterns
...Update README.rst:
Contributing
============
We welcome contributions! Please see:
- `CONTRIBUTING.rst <CONTRIBUTING.rst>`_ - General guidelines
- `Code Quality Standards <doc/source/contributor/code_quality.rst>`_
- `Code Patterns <doc/source/contributor/code_patterns.rst>`_
- `OpenStack Contributor Guide <https://docs.openstack.org/contributors/>`_
Development workflow:
.. code-block:: bash
# Install dependencies
pip install -r requirements.txt -r test-requirements.txt
# Install pre-commit hooks
pip install pre-commit
pre-commit install
# Run tests
tox -e py3
# Run quality checks
tox -e pep8
tox -e import-policy
tox -e reflection-policyUpdate CONTRIBUTING.rst:
Code Quality Standards
=======================
Before submitting changes, please review:
- `Code Quality Guide <doc/source/contributor/code_quality.rst>`_
- `Code Patterns <doc/source/contributor/code_patterns.rst>`_
Key requirements:
- Use module-level imports, not symbol imports
- Access Django settings via ``watcher_dashboard/config.py``
- Avoid reflection helpers (getattr/hasattr) for static attributes
- All code must pass CI checks
Running quality checks:
.. code-block:: bash
tox -e pep8 # Style checks
tox -e import-policy # Import compliance
tox -e reflection-policy # Reflection policy
tox -e py3 # Unit testsTasks:
- Create
doc/source/contributor/code_quality.rst - Create
doc/source/contributor/code_patterns.rst - Update
doc/source/contributor/index.rstto include both - Update
README.rstwith links - Update
CONTRIBUTING.rstwith standards - Build docs and verify
Testing:
# Build documentation
tox -e docs
# Check for warnings/errors
sphinx-build -W doc/source doc/build/html
# Verify cross-references
grep -r ":doc:" doc/source/contributor/code_*.rst
# Check RST syntax
doc8 doc/source/contributor/code_quality.rst
doc8 doc/source/contributor/code_patterns.rstCommit Message:
[Phase 4] Create comprehensive contributor documentation
Problem:
- Code quality policies established in phases 1-3
- Patterns exist in code but not documented for contributors
- New contributors need clear guidance on standards
- Code review checklist needed
Solution:
- Create doc/source/contributor/code_quality.rst (standards/policies)
- Create doc/source/contributor/code_patterns.rst (examples/patterns)
- Update contributor/index.rst to include new docs
- Update README.rst and CONTRIBUTING.rst with references
Files created:
- doc/source/contributor/code_quality.rst
* Import standards with rationale
* Configuration access via config module
* Reflection helper policy
* Testing standards and CI requirements
* Type hint guidelines
* Code review checklist
* Quick reference
- doc/source/contributor/code_patterns.rst
* Import patterns with examples
* Configuration access patterns
* Reflection helper patterns
* Testing patterns (mocking, etc.)
* Type hint patterns
* Common anti-patterns to avoid
Files modified:
- doc/source/contributor/index.rst (add to toctree)
- README.rst (add links to new docs)
- CONTRIBUTING.rst (add standards summary)
Benefits:
- Clear guidance for new and existing contributors
- Consistent code review standards
- Concrete examples for all patterns
- Reduced style discussions in PRs
- Serves as reference for all developers
Testing:
- tox -e docs passes without warnings
- All cross-references resolve correctly
- RST syntax validated with doc8
- Examples tested for accuracy
References:
- Phases 1-3 implementation
- Manila-UI patterns
- OpenStack Horizon best practices
Implements: code-quality-documentation
Partial-Implements: code-quality-improvement
Acceptance Criteria:
code_quality.rstcreated with comprehensive standardscode_patterns.rstcreated with concrete examples- Both docs in toctree and accessible
- README and CONTRIBUTING updated
- Documentation builds without warnings
- All cross-references work
- Examples are accurate
Goal: Add type hints progressively to improve static analysis and maintainability.
Duration: 1-2 weeks (ongoing)
Note: This phase is optional but highly recommended. The config module (Phase 3) already includes full type hints and serves as an example.
Scope: Set up mypy infrastructure without requiring immediate compliance.
Files to create:
mypy.inior add[tool.mypy]section topyproject.toml
Files to modify:
test-requirements.txt- Add mypy and type stubstox.ini- Add mypy environment
Configuration (add to pyproject.toml):
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false # Start permissive
check_untyped_defs = false # Gradually tighten
warn_redundant_casts = true
warn_unused_ignores = true
show_error_codes = true
namespace_packages = true
explicit_package_bases = true
# Ignore third-party packages without type stubs
[[tool.mypy.overrides]]
module = [
"horizon.*",
"openstack_dashboard.*",
"watcherclient.*",
]
ignore_missing_imports = truetest-requirements.txt:
mypy>=1.0.0
types-PyYAML
tox.ini:
[testenv:mypy]
description = Run mypy type checker on typed modules
deps =
{[testenv]deps}
mypy>=1.0.0
types-PyYAML
commands =
# Start with just config module (already 100% typed)
mypy watcher_dashboard/config.pyTesting:
# Install mypy
pip install mypy types-PyYAML
# Test on config module (should pass)
mypy watcher_dashboard/config.py
# Test via tox
tox -e mypyCommit Message:
[Phase 5.1] Configure mypy for progressive type checking
Problem:
- Config module (Phase 3) has type hints but no checking
- Want to enable gradual type hint adoption
- Need infrastructure before adding more type hints
Solution:
- Add mypy configuration to pyproject.toml
- Configure for permissive initial checking
- Ignore third-party packages without stubs
- Add tox environment for mypy checking
- Start with config module only (already typed)
Files created:
- [tool.mypy] section in pyproject.toml
Files modified:
- test-requirements.txt (add mypy, types-PyYAML)
- tox.ini (add [testenv:mypy])
Configuration:
- Python 3.10+ target
- Permissive settings for gradual adoption
- Third-party package ignores
- Initially only checks watcher_dashboard/config.py
Testing:
- mypy watcher_dashboard/config.py passes
- tox -e mypy passes
Next Steps:
- Subsequent commits will add type hints to modules
- Will expand mypy tox environment as modules are typed
- Not enforced in CI initially (optional check)
Documentation:
- See doc/source/contributor/code_quality.rst for type hint guidelines
- See doc/source/contributor/code_patterns.rst for examples
Partial-Implements: code-quality-type-hints
Suggested order:
watcher_dashboard/api/watcher.pywatcher_dashboard/common/client.py- Content modules as time permits
Pattern: Follow examples in code_patterns.rst and config.py
Files to create:
watcher_dashboard/py.typed(empty marker)
Files to modify:
setup.cfgorpyproject.toml- Include py.typed
Before Each Commit:
- All tests pass:
tox -e py3 - Style checks pass:
tox -e pep8 - Phase-specific checks pass
- Documentation updated
- Commit message follows template