Skip to content

Commit 6b9fdaf

Browse files
committed
Merge branch 'test-restructure'
1 parent e4e1b14 commit 6b9fdaf

File tree

21 files changed

+243
-132
lines changed

21 files changed

+243
-132
lines changed

.github/workflows/build-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name: Build and deploy
66

77
on:
88
push:
9-
branches: ["**"]
9+
branches: ["dev"]
1010
tags: ["*"]
1111
pull_request:
1212
branches: ["dev"]

.github/workflows/python-tests.yml

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,33 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- uses: actions/checkout@v4
16-
# https://github.com/actions/setup-python
17-
- name: Set up Python
18-
uses: actions/setup-python@v5
16+
- name: Install uv and set the python version
17+
id: setup-uv
18+
uses: astral-sh/setup-uv@v6
1919
with:
20-
python-version: '3.12'
21-
cache: 'pip' # caching pip dependencies
22-
# Manually set pyproject.toml as the file to use for dependencies
23-
# Workaround while waiting for https://github.com/actions/setup-python/issues/529
24-
cache-dependency-path: 'pyproject.toml'
25-
- name: Run pre-commit
26-
uses: pre-commit/[email protected]
27-
- uses: pre-commit-ci/[email protected]
28-
if: always()
20+
version: "0.7.19" # It is considered best practice to pin to a specific uv version
21+
# https://github.com/astral-sh/setup-uv?tab=readme-ov-file#enable-caching
22+
enable-cache: true
23+
cache-dependency-glob: |
24+
**/*requirements*.txt
25+
**/pyproject.toml
26+
**/uv.lock
27+
28+
- name: Do something if the cache was restored
29+
if: steps.setup-uv.outputs.cache-hit == 'true'
30+
run: echo "Cache was restored"
31+
32+
- name: Install the project
33+
run: uv sync --all-extras --dev
34+
35+
- name: Run pre-commit hooks
36+
run: uv run pre-commit run --all-files --show-diff-on-failure --color=always
37+
38+
# - uses: pre-commit-ci/[email protected]
39+
# if: always()
2940

3041
test:
42+
needs: lint
3143
runs-on: ubuntu-latest
3244
strategy:
3345
fail-fast: false
@@ -36,18 +48,46 @@ jobs:
3648

3749
steps:
3850
- uses: actions/checkout@v4
39-
# https://github.com/actions/setup-python
40-
- name: Set up Python ${{ matrix.python-version }}
41-
uses: actions/setup-python@v5
51+
- name: Install uv and set the python version
52+
id: setup-uv
53+
uses: astral-sh/setup-uv@v6
4254
with:
55+
# https://docs.astral.sh/uv/guides/integration/github/#multiple-python-versions
4356
python-version: ${{ matrix.python-version }}
44-
cache: 'pip' # caching pip dependencies
45-
# Manually set 'dev-requirements.txt' as the file to use for dependencies, since `requirements.txt` contains runtime dependencies.
46-
cache-dependency-path: 'dev-requirements.txt'
47-
- name: Install dependencies
48-
run: |
49-
python -m pip install --upgrade pip
50-
pip install -r dev-requirements.txt
51-
- name: Test with pytest
57+
version: "0.7.19" # It is considered best practice to pin to a specific uv version
58+
# https://github.com/astral-sh/setup-uv?tab=readme-ov-file#enable-caching
59+
enable-cache: true
60+
cache-dependency-glob: |
61+
**/*requirements*.txt
62+
**/pyproject.toml
63+
**/uv.lock
64+
65+
- name: Do something if the cache was restored
66+
if: steps.setup-uv.outputs.cache-hit == 'true'
67+
run: echo "Cache was restored"
68+
69+
- name: Install the project
70+
run: uv sync --all-extras --dev
71+
72+
- name: Run tests
73+
# For example, using `pytest`
5274
run: |
53-
python -m pytest
75+
uv run pytest tests/unit
76+
uv run pytest -m ci
77+
78+
# # https://github.com/actions/setup-python
79+
# - name: Set up Python ${{ matrix.python-version }}
80+
# uses: actions/setup-python@v5
81+
# with:
82+
# python-version: ${{ matrix.python-version }}
83+
# cache: 'pip' # caching pip dependencies
84+
# # Manually set 'dev-requirements.txt' as the file to use for dependencies, since `requirements.txt` contains runtime dependencies.
85+
# cache-dependency-path: 'dev-requirements.txt'
86+
# - name: Install dependencies
87+
# run: |
88+
# python -m pip install --upgrade pip
89+
# pip install -r dev-requirements.txt
90+
# - name: Test with pytest
91+
# run: |
92+
# python -m pytest tests/unit
93+
# python -m pytest -m ci

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ target/
6565
venv
6666
.venv
6767
uv.lock
68+
*.ipynb
6869

6970
# Mac stuff
7071
.DS_Store

appdaemon/logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ def get_time(logger, record, format=None):
363363
ts = logger.AD.sched.get_now_sync().astimezone(logger.tz)
364364
else:
365365
if logger.tz is not None:
366-
ts = datetime.datetime.now(datetime.UTC).astimezone(logger.tz)
366+
ts = datetime.datetime.now(datetime.timezone.utc).astimezone(logger.tz)
367367
else:
368368
ts = datetime.datetime.now()
369369
if format is not None:

appdaemon/scheduler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import uuid
77
from collections import OrderedDict
88
from copy import deepcopy
9-
from datetime import UTC, datetime, time, timedelta, timezone
9+
from datetime import datetime, time, timedelta, timezone
1010
from logging import Logger
1111
from typing import TYPE_CHECKING, Any, Callable
1212

@@ -87,7 +87,7 @@ def stop(self):
8787
self.stopping = True
8888

8989
async def set_start_time(self, starttime: datetime | None = None):
90-
self.last_fired = datetime.now(UTC).astimezone(self.AD.tz)
90+
self.last_fired = datetime.now(timezone.utc).astimezone(self.AD.tz)
9191
if not self.AD.real_time:
9292
self.logger.info("Starting time travel ...")
9393
self.logger.info("Setting clocks to %s", await self.get_now())

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ asyncio_default_fixture_loop_scope = "session"
9797
addopts = [
9898
"--import-mode=importlib",
9999
]
100+
markers = [
101+
"ci: mark test to run in CI environment",
102+
]
100103

101104
# black configuration
102105
[tool.black]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
run_every:
22
module: run_every
33
class: RunEvery
4+
disable: true
5+
6+
run_every_now:
7+
module: run_every
8+
class: RunEveryNow
9+
disable: true
10+
interval: 0.75
11+
msg: "run_every_now"
412

513
run_hourly:
614
module: run_every
715
class: RunHourly
16+
disable: true

tests/conf/apps/scheduler/run_every.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from datetime import timedelta
22

3-
from appdaemon import ADAPI
3+
from appdaemon import utils
4+
from appdaemon.adapi import ADAPI
5+
from appdaemon.adbase import ADBase
46

57

68
class RunEvery(ADAPI):
@@ -17,15 +19,36 @@ def start_realtime(self) -> None:
1719
self.run_every(self.scheduled_callback, start="now", interval=timedelta(seconds=1), data="start now")
1820

1921
def start_timewarp(self) -> None:
20-
self.run_every(self.scheduled_callback, start="now", interval="45:00", data="start now")
21-
self.run_every(self.scheduled_callback, start="now + 5:00", interval=timedelta(hours=1.37), data="start later")
22+
# self.run_every(self.scheduled_callback, start="now", interval="02:37:45.7", data="start now")
23+
self.run_every(self.scheduled_callback, start="now + 02:00:00", interval=timedelta(hours=1), data="start later")
2224
# self.run_every(self.scheduled_callback, start="sunrise", data='sunrise')
2325
# self.run_every(self.scheduled_callback, start="sunrise - 01:00:00", data='sunrise negative offset')
2426

2527
def scheduled_callback(self, data: str, **kwargs) -> None:
2628
self.log(f"{data}", level="DEBUG")
2729

2830

31+
class RunEveryNow(ADBase):
32+
adapi: ADAPI
33+
34+
def initialize(self) -> None:
35+
self.adapi = self.get_ad_api()
36+
self.adapi.set_log_level("DEBUG")
37+
self.adapi.run_every(
38+
self.scheduled_callback,
39+
start="now",
40+
interval=self.interval,
41+
data=self.args['msg']
42+
)
43+
44+
def scheduled_callback(self, data: str, **kwargs) -> None:
45+
self.adapi.log(f"{data}", level="DEBUG")
46+
47+
@property
48+
def interval(self) -> timedelta:
49+
return utils.parse_timedelta(self.args["interval"])
50+
51+
2952
class RunHourly(RunEvery):
3053
def initialize(self) -> None:
3154
# self.set_log_level("DEBUG")

tests/conftest.py

Lines changed: 13 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import asyncio
22
import logging
3-
from datetime import date, datetime, time
4-
from functools import partial
5-
from signal import Signals, raise_signal
6-
from typing import Callable
3+
from datetime import datetime
74

85
import pytest
96
import pytest_asyncio
107
from astral import LocationInfo
118
from astral.location import Location
12-
from pytz import BaseTzInfo, timezone
139

14-
from appdaemon import AppDaemon, utils
10+
from appdaemon import AppDaemon
1511
from appdaemon.logging import Logging
1612
from appdaemon.models.config.appdaemon import AppDaemonConfig
1713

@@ -62,7 +58,7 @@ def ad_cfg() -> AppDaemonConfig:
6258
module_debug={
6359
"_app_management": "DEBUG",
6460
# "_events": "DEBUG",
65-
# "_utility": "DEBUG",
61+
"_utility": "DEBUG",
6662
},
6763
)
6864
)
@@ -88,18 +84,20 @@ async def ad_obj(logging_obj: Logging, running_loop, ad_cfg: AppDaemonConfig):
8884
# This can't be done here because the test might set the app directory to a different location
8985
# await ad.app_management.check_app_updates(mode=UpdateMode.TESTING)
9086

91-
# ad.start()
87+
ad.start()
9288
yield ad
9389
logger.info('Back to fixture scope, stopping AppDaemon')
94-
pass
90+
if stopping_tasks := ad.stop():
91+
logger.debug("Waiting for stopping tasks to complete")
92+
await stopping_tasks
9593

9694

97-
@pytest.fixture(scope="module")
98-
def ad_obj_fast(logging_obj: Logging, running_loop, ad_cfg: AppDaemonConfig):
95+
@pytest_asyncio.fixture(scope="module")
96+
async def ad_obj_fast(logging_obj: Logging, running_loop, ad_cfg: AppDaemonConfig):
9997
logger = logging.getLogger("AppDaemon._test")
10098
logger.info(f"Passed loop: {hex(id(running_loop))}")
10199

102-
ad_cfg.timewarp = 5000
100+
ad_cfg.timewarp = 2000
103101
ad_cfg.starttime = ad_cfg.time_zone.localize(datetime(2025, 6, 25, 0, 0, 0))
104102

105103
ad = AppDaemon(
@@ -113,10 +111,11 @@ def ad_obj_fast(logging_obj: Logging, running_loop, ad_cfg: AppDaemonConfig):
113111
logger.propagate = True
114112
logger.setLevel("DEBUG")
115113

116-
ad.start()
114+
# ad.start()
117115
yield ad
118-
raise_signal(Signals.SIGTERM)
116+
# raise_signal(Signals.SIGTERM)
119117
# ad.stop()
118+
pass
120119

121120

122121
@pytest.fixture
@@ -130,63 +129,3 @@ def location() -> Location:
130129
longitude=-74.0060,
131130
)
132131
)
133-
134-
135-
@pytest.fixture
136-
def tz(location: Location) -> BaseTzInfo:
137-
return timezone(location.timezone)
138-
139-
140-
@pytest.fixture
141-
def default_date() -> date:
142-
return date(2025, 6, 20)
143-
144-
145-
@pytest.fixture
146-
def tomorrow_date(default_date: date) -> date:
147-
return default_date.replace(day=default_date.day + 1)
148-
149-
150-
@pytest.fixture
151-
def now_creator(default_date: date, tz: BaseTzInfo):
152-
def create_time(hour: int):
153-
naive = datetime.combine(default_date, time(hour, 0, 0))
154-
return tz.localize(naive)
155-
156-
return create_time
157-
158-
159-
@pytest.fixture
160-
def early_now(now_creator: Callable[..., datetime]) -> datetime:
161-
now = now_creator(4)
162-
assert now.isoformat() == "2025-06-20T04:00:00-04:00"
163-
return now
164-
165-
166-
@pytest.fixture
167-
def default_now(now_creator: Callable[..., datetime]) -> datetime:
168-
now = now_creator(12)
169-
assert now.isoformat() == "2025-06-20T12:00:00-04:00"
170-
return now
171-
172-
173-
@pytest.fixture
174-
def late_now(now_creator: Callable[..., datetime]) -> datetime:
175-
now = now_creator(23)
176-
assert now.isoformat() == "2025-06-20T23:00:00-04:00"
177-
return now
178-
179-
180-
@pytest.fixture
181-
def parser(tz: BaseTzInfo, default_now: datetime) -> partial[datetime]:
182-
return partial(utils.parse_datetime, now=default_now, timezone=tz)
183-
184-
185-
@pytest.fixture
186-
def parser_location(tz: BaseTzInfo, location: Location) -> partial[datetime]:
187-
return partial(utils.parse_datetime, location=location, timezone=tz)
188-
189-
190-
@pytest.fixture
191-
def time_at_elevation(location: Location, default_now: datetime) -> Callable[..., datetime]:
192-
return partial(location.time_at_elevation, date=default_now.date(), local=True)

tests/functional/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)