Skip to content

Commit f4638ca

Browse files
authored
Merge pull request #4 from pvaret/minor-fixes
Plethora of minor fixes.
2 parents cd13bfd + e9ee72c commit f4638ca

21 files changed

+191
-127
lines changed

.github/workflows/python-build.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ jobs:
2727
with:
2828
python-version: ${{ matrix.python-version }}
2929

30+
- name: Install UV
31+
uses: astral-sh/setup-uv@v7
32+
3033
- name: Install Hatch
31-
uses: pypa/hatch@install
34+
run: uv tool install hatch
3235

3336
- name: Report Hatch status
3437
run: hatch status
@@ -40,7 +43,7 @@ jobs:
4043
run: hatch fmt --formatter --check
4144

4245
- name: Check types
43-
run: hatch run dev:pyright --verbose
46+
run: hatch run dev:mypy
4447

4548
- name: Run test suite
4649
run: hatch test -py=${{ matrix.python-version }} --slow

docs/source/changelog.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Latest
66

77
Nothing yet.
88

9-
SunsetSettings 0.7.o (2025-06-29)
9+
SunsetSettings 0.7.0 (2025-06-29)
1010
---------------------------------
1111

1212
- Added :code:`AutoLoader` context manager, which reloads the settings file when it is

pyproject.toml

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,20 @@ Documentation = "https://sunsetsettings.readthedocs.io/"
3636
[project.optional-dependencies]
3737
tests = ["coverage[toml]", "pytest", "pytest-cov", "pytest-mock", "pytest-skip-slow"]
3838
docs = ["sphinx", "sphinx_rtd_theme"]
39-
40-
[tool.hatch.build.targets.sdist]
41-
only-include = ["sunset/", "tests/", "docs/"]
42-
43-
[tool.hatch.build.targets.wheel]
44-
packages = ["sunset/"]
39+
dev = ["mypy", "ruff", "ty"]
4540

4641
[tool.pytest.ini_options]
47-
addopts = "-vv --doctest-modules --doctest-glob=README.md --doctest-glob=docs/*.rst --cov --cov-branch --cov-report=xml"
42+
addopts = [
43+
"--import-mode=importlib",
44+
"-vv",
45+
"--doctest-modules",
46+
"--doctest-glob=README.md",
47+
"--doctest-glob=docs/*.rst",
48+
"--cov",
49+
"--cov-branch",
50+
"--cov-report=term-missing",
51+
"--cov-report=xml",
52+
]
4853

4954
[tool.coverage.run]
5055
# Also measure tests. Helps detect tests that are not properly executed.
@@ -63,6 +68,13 @@ exclude_also = [
6368
]
6469
omit = ["tests/demo_*.py"]
6570

71+
[tool.mypy]
72+
packages = ["sunset"]
73+
strict = true
74+
show_error_context = true
75+
show_error_code_links = true
76+
pretty = true
77+
6678
[tool.ruff]
6779
# Use the default line length.
6880
line-length = 88
@@ -87,6 +99,12 @@ docstring-code-format = false
8799
[tool.hatch.version]
88100
path = "sunset/__init__.py"
89101

102+
[tool.hatch.build.targets.sdist]
103+
only-include = ["sunset/", "tests/", "docs/"]
104+
105+
[tool.hatch.build.targets.wheel]
106+
packages = ["sunset"]
107+
90108
[tool.hatch.envs.hatch-test]
91109
# This forces hatch test to use the pytest ini options listed above.
92110
default-args = []
@@ -106,14 +124,16 @@ python = ["3.10", "3.11", "3.12", "3.13"]
106124
# Use this environment for development purposes. I.e. point VSCode to the Python
107125
# interpreter in this environment.
108126

109-
# Add test-related dependencies to the dev environment.
110-
features = ["tests"]
111-
112-
# This environment is also where we exercise type checks.
113-
dependencies = ["pyright"]
127+
# Add all dev-related dependencies to the dev environment.
128+
features = ["dev", "tests", "docs"]
114129

115-
# This environment does not need the code built and installed.
116-
detached = true
130+
[tool.hatch.envs.dev.scripts]
131+
check-all = """\
132+
hatch run hatch-static-analysis:format-check \
133+
&& hatch run hatch-static-analysis:lint-check \
134+
&& hatch run dev:ty check\
135+
&& hatch run dev:mypy \
136+
"""
117137

118138
[tool.hatch.envs.docs]
119139
# This makes this environment use the "docs" optional dependencies above.
@@ -126,8 +146,9 @@ detached = true
126146
html = "make -C ./docs html"
127147

128148
[tool.hatch.envs.hatch-static-analysis]
129-
# Require a version of Ruff that agrees with the linting parameters above.
130-
dependencies = ["ruff>=0.12.1"]
149+
# Use the version of Ruff that's in our dev environment. Don't add it as a special dependency.
150+
features = ["dev"]
151+
dependencies = []
131152

132153
# Disable Hatch's custom Ruff defaults so that our linting parameters above are the
133154
# entirety of what is used.

sunset/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
from sunset.autosaver import AutoSaver
1313
from sunset.bunch import Bunch
1414
from sunset.enum_serializer import SerializableEnum, SerializableFlag
15+
from sunset.exporter import normalize
1516
from sunset.key import Key
1617
from sunset.list import List
1718
from sunset.protocols import Serializable, Serializer
18-
from sunset.settings import Settings, normalize
19+
from sunset.settings import Settings
1920
from sunset.timer import PersistentTimer
2021

2122

sunset/autoloader.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44
from pathlib import Path
55
from typing import IO, TYPE_CHECKING, Protocol
66

7-
from .timer import PersistentTimer
7+
from .timer import PersistentTimer, TimerFactory, TimerProtocol
88

99
if TYPE_CHECKING: # pragma: no cover
10+
import sys
1011
from collections.abc import Callable
1112
from types import TracebackType
12-
from typing import Any, Self
13+
from typing import Any
14+
15+
if sys.version_info < (3, 11):
16+
from typing_extensions import Self
17+
else:
18+
from typing import Self
1319

1420
from sunset import Settings
1521

@@ -43,18 +49,21 @@ class MonitorForChange:
4349
_path: Path
4450
_callback: Callable[[], Any]
4551
_monitor_period_s: int
46-
_timer: PersistentTimer | None = None
52+
_timer: TimerProtocol | None = None
53+
_timer_factory: TimerFactory = PersistentTimer
4754

4855
def __init__(
4956
self,
5057
path: Path,
5158
callback: Callable[[], Any],
5259
monitor_period_s: int = 1,
60+
_timer_factory: TimerFactory | None = None,
5361
) -> None:
5462
self._path = path
5563
self._callback = callback
5664
self._monitor_period_s = monitor_period_s
57-
self._timer = None
65+
if _timer_factory is not None:
66+
self._timer_factory = _timer_factory
5867

5968
def start(self) -> None:
6069
if self._timer is not None:
@@ -83,7 +92,7 @@ def loop() -> None:
8392
pending_update = True
8493
last_modified = new_last_modified
8594

86-
self._timer = PersistentTimer(looping=True)
95+
self._timer = self._timer_factory(looping=True)
8796
self._timer.start(self._monitor_period_s, loop)
8897

8998
def stop(self) -> None:

sunset/autosaver.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import sys
23
import tempfile
34
from collections.abc import Callable
45
from pathlib import Path
@@ -8,6 +9,11 @@
89
from .autoloader import LoadableProtocol, doLoad
910
from .timer import PersistentTimer, TimerProtocol
1011

12+
if sys.version_info < (3, 11): # pragma: no cover
13+
from typing_extensions import Self
14+
else:
15+
from typing import Self
16+
1117

1218
class SavableProtocol(LoadableProtocol, Protocol):
1319
"""
@@ -239,7 +245,7 @@ def __del__(self) -> None:
239245
if self._save_on_delete:
240246
self.saveIfNeeded()
241247

242-
def __enter__(self) -> "AutoSaver":
248+
def __enter__(self) -> Self:
243249
return self
244250

245251
def __exit__(

sunset/bunch.py

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from sunset.lock import SettingsLock
1717
from sunset.notifier import Notifier
18-
from sunset.protocols import BaseField, Field, ItemTemplate, UpdateNotifier
18+
from sunset.protocols import BaseField, Field, UpdateNotifier
1919
from sunset.sets import WeakNonHashableSet
2020
from sunset.stringutils import collate_by_prefix, split_on
2121

@@ -81,16 +81,12 @@ def __new__(cls) -> Self:
8181
# one here.
8282

8383
cls_parents = cls.__bases__
84-
dataclass_fields: list[tuple[str, type | GenericAlias, ItemTemplate]] = []
85-
potential_fields = [
86-
(name, getattr(cls, name, None))
87-
for name in dir(cls)
88-
if not name.startswith("__")
89-
]
84+
dataclass_fields: list[tuple[str, type | GenericAlias, Field]] = []
85+
potential_fields = [(name, getattr(cls, name, None)) for name in dir(cls)]
9086

9187
for name, attr in potential_fields:
92-
if inspect.isclass(attr):
93-
if attr.__name__ == name:
88+
if inspect.isclass(attr) and issubclass(attr, Bunch):
89+
if name in (attr.__name__, dataclass_attr):
9490
# This is probably a class definition that just happens
9591
# to be located inside the containing Bunch definition.
9692
# This is fine.
@@ -99,29 +95,32 @@ def __new__(cls) -> Self:
9995

10096
msg = (
10197
f"Field '{name}' in the definition of '{cls.__name__}' is"
102-
" uninstantiated. Did you forget the parentheses?"
98+
f" uninstantiated. Did you mean '{attr.__name__}()'?"
10399
)
104100
raise TypeError(msg)
105101

106-
if isinstance(attr, ItemTemplate):
107-
# Safety check: make sure the user isn't accidentally overriding an
108-
# existing attribute. We do however allow overriding an attribute
109-
# with an attribute of the same type, which allows the user to
110-
# override a Key with a Key of the same type but a different
111-
# default value, for instance.
112-
113-
for cls_parent in cls_parents:
114-
if (
115-
parent_attr := getattr(cls_parent, name, None)
116-
) is not None and type(parent_attr) is not type(attr):
117-
msg = (
118-
f"Field '{name}' in the definition of"
119-
f" '{cls.__name__}' overrides attribute of the"
120-
" same name declared in parent class"
121-
f" '{cls_parent.__name__}'; consider renaming"
122-
f" this field to '{name}_' for instance"
123-
)
124-
raise TypeError(msg)
102+
if not isinstance(attr, Field):
103+
# Not actually a field, then.
104+
continue
105+
106+
# Safety check: make sure the user isn't accidentally overriding an
107+
# existing attribute. We do however allow overriding an attribute
108+
# with an attribute of the same type, which allows the user to
109+
# override a Key with a Key of the same type but a different
110+
# default value, for instance.
111+
112+
for cls_parent in cls_parents:
113+
if (
114+
parent_attr := getattr(cls_parent, name, None)
115+
) is not None and type(parent_attr) is not type(attr):
116+
msg = (
117+
f"Field '{name}' in the definition of"
118+
f" '{cls.__name__}' overrides attribute of the"
119+
" same name declared in parent class"
120+
f" '{cls_parent.__name__}'; consider renaming"
121+
f" this field to '{name}_' for instance"
122+
)
123+
raise TypeError(msg)
125124

126125
# Create a proper field from the attribute.
127126

@@ -156,7 +155,7 @@ def __new__(cls) -> Self:
156155

157156
# Create an instance of the dataclass.
158157

159-
new_cls = super().__new__(dataclass_class)
158+
new_cls: Self = super().__new__(dataclass_class)
160159

161160
# Set up the fields that were identified above as instance attributes.
162161

@@ -321,7 +320,8 @@ def isSet(self) -> bool:
321320

322321
@SettingsLock.with_write_lock
323322
def clear(self) -> None:
324-
[field.clear() for field in self._fields.values()]
323+
for field in self._fields.values():
324+
field.clear()
325325

326326
@SettingsLock.with_read_lock
327327
def dumpFields(self) -> Iterable[tuple[str, str | None]]:

sunset/key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
_T = TypeVar("_T")
2525

2626

27-
class Key(Generic[_T], BaseField):
27+
class Key(BaseField, Generic[_T]):
2828
"""
2929
A single setting key containing a typed value.
3030

sunset/list.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,13 @@
1010
else:
1111
from typing import Self
1212

13-
from sunset.bunch import Bunch
14-
from sunset.key import Key
1513
from sunset.lock import SettingsLock
1614
from sunset.notifier import Notifier
17-
from sunset.protocols import BaseField, UpdateNotifier
15+
from sunset.protocols import BaseField, Field, UpdateNotifier
1816
from sunset.sets import WeakNonHashableSet
1917
from sunset.stringutils import collate_by_prefix, split_on
2018

21-
ListItemT = TypeVar("ListItemT", bound=Bunch | Key[Any])
19+
ListItemT = TypeVar("ListItemT", bound=Field)
2220

2321

2422
class IterOrder(Enum):
@@ -130,6 +128,7 @@ def __setitem__(
130128
self._clearMetadata(self._contents[index])
131129
if isinstance(index, slice):
132130
assert isinstance(value, Iterable) # noqa: S101
131+
assert not isinstance(value, Field) # noqa: S101
133132
self._contents[index] = value
134133

135134
else:

sunset/protocols.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def fromStr(cls: type[Self], string: str) -> Self | None:
4848
...
4949

5050

51-
class Serializer(Generic[_T], Protocol):
51+
class Serializer(Protocol, Generic[_T]):
5252
"""
5353
A protocol that describes a way to serialize and deserialize an arbitrary
5454
type.

0 commit comments

Comments
 (0)