Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
if: needs.lint.result == 'success'
strategy:
matrix:
python-version: ['3.10']
python-version: ['3.11']
steps:
- uses: actions/checkout@v5
- name: Get history and tags for SCM versioning to work
Expand All @@ -77,14 +77,12 @@ jobs:
- name: Install project
run: |
pip install .
- name: Run Mypy
run: |
pip install mypy pytest
mypy atom
- name: Test with pytest
- name: Validate static assertions
run: |
pip install pytest-mypy-plugins regex
python -X dev -m pytest tests/type_checking -v
pip install -r test_requirements.txt
python -X dev -m mypy tests/type_checking --config-file pyproject.toml
ty check tests/type_checking
python -X dev -m pyrefly check tests/type_checking
tests:
name: Unit tests
runs-on: ${{ matrix.os }}
Expand All @@ -94,7 +92,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15-dev']
python-version: ['3.11', '3.12', '3.13', '3.14', '3.15-dev']
steps:
- uses: actions/checkout@v5
- name: Get history and tags for SCM versioning to work
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version: 2
build:
os: ubuntu-20.04
tools:
python: "3.10"
python: "3.11"
apt_packages:
- graphviz

Expand Down
10 changes: 7 additions & 3 deletions atom/catom.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ from typing import (
List,
Literal,
Optional,
Self,
Sequence,
Set,
Tuple,
Expand All @@ -22,8 +23,6 @@ from typing import (
overload,
)

from typing_extensions import Self

from .atom import Atom
from .property import Property
from .typing_utils import ChangeDict
Expand Down Expand Up @@ -76,7 +75,12 @@ class Member(Generic[T, S]):
setattr_mode: Tuple[SetAttr, Any] = ...
validate_mode: Tuple[Validate, Any] = ...
getstate_mode: Tuple[GetState, Any] = ...
def __init__(self) -> None: ...
# Runtime constructors normalize a broad set of keyword and positional arguments
# before delegating to the validation machinery. The public stubs intentionally keep
# this initializer permissive while the specialized __new__ overloads capture the
# type-level API contract. This avoids duplicating the runtime normalization logic in
# the .pyi files while still allowing the checker to validate the supported calls.
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
@overload
def __get__(self, instance: None, owner: Type[Atom]) -> Self: ...
@overload
Expand Down
3 changes: 0 additions & 3 deletions atom/src/atomdict.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,7 @@ int AtomDict_traverse( AtomDict* self, visitproc visit, void* arg )
{
Py_VISIT( self->m_key_validator );
Py_VISIT( self->m_value_validator );
#if PY_VERSION_HEX >= 0x03090000
// This was not needed before Python 3.9 (Python issue 35810 and 40217)
Py_VISIT(Py_TYPE(self));
#endif
// PyDict_type is not heap allocated so it does visit the type
return PyDict_Type.tp_traverse( pyobject_cast( self ), visit, arg );
}
Expand Down
3 changes: 0 additions & 3 deletions atom/src/catom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,7 @@ CAtom_traverse( CAtom* self, visitproc visit, void* arg )
{
Py_VISIT( self->slots[ i ] );
}
#if PY_VERSION_HEX >= 0x03090000
// This was not needed before Python 3.9 (Python issue 35810 and 40217)
Py_VISIT(Py_TYPE(self));
#endif
if( self->observers )
{
return self->observers->py_traverse( visit, arg );
Expand Down
3 changes: 0 additions & 3 deletions atom/src/member.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,7 @@ Member_traverse( Member* self, visitproc visit, void* arg )
for( it = self->static_observers->begin(); it != end; ++it )
Py_VISIT( it->m_observer.get() );
}
#if PY_VERSION_HEX >= 0x03090000
// This was not needed before Python 3.9 (Python issue 35810 and 40217)
Py_VISIT(Py_TYPE(self));
#endif
return 0;
}

Expand Down
3 changes: 1 addition & 2 deletions atom/tuple.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ from typing import (
Type,
TypeVar,
Union,
Unpack,
overload,
)

from typing_extensions import Unpack

from .catom import Member

T = TypeVar("T")
Expand Down
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,21 @@
name = "atom"
description = "Memory efficient Python objects"
readme = "README.rst"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = "BSD-3-Clause"
license-files = ["LICENSE"]
authors = [{ name = "The Nucleic Development Team", email = "sccolbert@gmail.com" }]
maintainers = [{ name = "Matthieu C. Dartiailh", email = "m.dartiailh@gmail.com" }]
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Programming Language :: Python :: Implementation :: CPython",
]
dependencies = ["typing_extensions;python_version<'3.11'"]
dynamic = ["version"]

[project.urls]
Expand Down
4 changes: 3 additions & 1 deletion test_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mypy
pyrefly
ty
pytest
pytest-cov
pytest-mypy-plugins
pytest-benchmark
psutil
13 changes: 13 additions & 0 deletions tests/type_checking/annotations_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import assert_type

from atom.api import Atom, List


# Annotated member declarations should expose the Atom descriptor type on the class
# and the corresponding concrete Python container on the instance.
class A(Atom):
m: List[int] = List()


assert_type(A.m, List[int])
assert_type(A().m, list[int])
45 changes: 45 additions & 0 deletions tests/type_checking/coerced_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import _io
from typing import assert_type

from atom.api import Atom, Coerced


# Coerced members validate the same class/instance split as the other descriptors: the
# class exposes the Coerced[...] descriptor, while the instance value remains the concrete
# runtime type after coercion. Each class below exercises a distinct coercion branch.
def g() -> _io.StringIO:
return _io.StringIO()


class ACoercedScalar(Atom):
# A scalar coercion target preserves the exact runtime type and keeps the descriptor
# parameterized with that scalar.
m = Coerced(int)


class ACoercedTuple(Atom):
# A tuple of valid runtime types widens the coercion descriptor to the corresponding
# union while the instance still stores only one concrete type.
m = Coerced((int, float))


class ACoercedStringIO(Atom):
# StringIO inputs with a default factory-like initialization should still resolve to the
# concrete StringIO type on the instance.
m = Coerced(_io.StringIO, kwargs={"initial_value": "1"})


class ACoercedFactory(Atom):
# Factory-based coercion should behave the same as the other direct object coercion
# scenarios and remain concrete on the instance even though the descriptor is generic.
m = Coerced(_io.StringIO, factory=g)


assert_type(ACoercedScalar.m, Coerced[int, int])
assert_type(ACoercedScalar().m, int)
assert_type(ACoercedTuple.m, Coerced[int | float, int | float])
assert_type(ACoercedTuple().m, int | float)
assert_type(ACoercedStringIO.m, Coerced[_io.StringIO, _io.StringIO])
assert_type(ACoercedStringIO().m, _io.StringIO)
assert_type(ACoercedFactory.m, Coerced[_io.StringIO, _io.StringIO])
assert_type(ACoercedFactory().m, _io.StringIO)
14 changes: 14 additions & 0 deletions tests/type_checking/delegator_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import assert_type

from atom.api import Atom, Delegator, Int


# Delegator retains the descriptor wrapper on the class but exposes the delegated
# value on the instance.
class A(Atom):
i = Int(strict=False)
m = Delegator(i)


assert_type(A.m, Delegator[int, int | float])
assert_type(A().m, int)
141 changes: 141 additions & 0 deletions tests/type_checking/dict_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from typing import Any, assert_type

from atom.api import Atom, Dict, Int


# Dict members are checked along both axes: the class exposes Dict[key_type, value_type]
# and the instance exposes dict[key_type, value_type]. The classes below cover the main
# key/value inference combinations without depending on a generated fixture matrix.
class AUnspecified(Atom):
# No key or value constraints means the descriptor stays completely generic.
m = Dict()


class ADefaultDict(Atom):
# The default value is only a factory for instance state; it does not widen the type
# declaration itself.
m = Dict(default={"a": 1})


class AKeyScalar(Atom):
# Key-only typing narrows only the key side and leaves the value side free.
m = Dict(int)


class AKeyTuple(Atom):
# A tuple of key types creates a key union and should propagate to the descriptor.
m = Dict((int, float, str))


class AValueScalar(Atom):
# Value-only typing narrows only the value side while the key remains unconstrained.
m = Dict(None, int)


class AValueTuple(Atom):
# Tuple value types widen to the same union semantics used elsewhere in Atom.
m = Dict(None, (int, float, str))


class AKeywordValue(Atom):
# The keyword form is equivalent to positional value specification; it still narrows
# only the value side.
m = Dict(value=(int, float, str))


class AKeyValueScalar(Atom):
# Fully specified key/value typing should produce the precise Dict[int, int] result.
m = Dict(int, int)


class AKeyValueTuple(Atom):
# Tuple-based keys and values produce union inference on both sides of the dictionary.
m = Dict((int, str), (int, float))


class AKeyValueTripleTuple(Atom):
# Three-way key and value unions keep the same shape but widen the key/value unions to
# their combined set of valid types.
m = Dict((int, str, bytes), (int, float, str))


class AMemberKey(Atom):
# Member-based key specification should resolve to the member's concrete type and keep
# the value side typed independently.
m = Dict(Int(), (int, float))


assert_type(AUnspecified.m, Dict[Any, Any])
assert_type(AUnspecified().m, dict[Any, Any])
assert_type(ADefaultDict.m, Dict[Any, Any])
assert_type(ADefaultDict().m, dict[Any, Any])
assert_type(AKeyScalar.m, Dict[int, Any])
assert_type(AKeyScalar().m, dict[int, Any])
assert_type(AKeyTuple.m, Dict[int | float | str, Any])
assert_type(AKeyTuple().m, dict[int | float | str, Any])
assert_type(AValueScalar.m, Dict[Any, int])
assert_type(AValueScalar().m, dict[Any, int])
assert_type(AValueTuple.m, Dict[Any, int | float | str])
assert_type(AValueTuple().m, dict[Any, int | float | str])
assert_type(AKeywordValue.m, Dict[Any, int | float | str])
assert_type(AKeywordValue().m, dict[Any, int | float | str])
assert_type(AKeyValueScalar.m, Dict[int, int])
assert_type(AKeyValueScalar().m, dict[int, int])
assert_type(AKeyValueTuple.m, Dict[int | str, int | float])
assert_type(AKeyValueTuple().m, dict[int | str, int | float])
assert_type(AKeyValueTripleTuple.m, Dict[int | str | bytes, int | float | str])
assert_type(AKeyValueTripleTuple().m, dict[int | str | bytes, int | float | str])
assert_type(AMemberKey.m, Dict[int, int | float])
assert_type(AMemberKey().m, dict[int, int | float])


# The additional classes below exercise the single-element tuple variants and the member-
# based key/value shorthand that are easy to miss in a manual suite. They are redundant in
# meaning but important as coverage for the constructor overloads.
class AKeyOneTuple(Atom):
# A one-element tuple key is equivalent to the scalar key type case and should collapse
# to a single key type.
m = Dict((int,), int)


class AKeyTwoTuple(Atom):
# Two-element tuple keys widen the dictionary key union to the union of both member
# types while the value side remains fixed.
m = Dict((int, str), int)


class AKeyThreeTuple(Atom):
# Three-element tuple keys widen to the three-way union while preserving the value
# typing contract.
m = Dict((int, str, bytes), int)


class AValueFromIntMember(Atom):
# Value-side Int() members should infer as int exactly like the scalar int case.
m = Dict(None, Int())


class AKeywordValueFromIntMember(Atom):
# The keyword value form with an Int() member is covered separately to ensure the
# overload resolution remains the same as the positional equivalent.
m = Dict(value=Int())


class AMemberAsKeyAndValue(Atom):
# Using a member on both sides verifies that the key and value validation paths each
# resolve to the member's concrete type without cross-contaminating the other side.
m = Dict(Int(), Int())


assert_type(AKeyOneTuple.m, Dict[int, int])
assert_type(AKeyOneTuple().m, dict[int, int])
assert_type(AKeyTwoTuple.m, Dict[int | str, int])
assert_type(AKeyTwoTuple().m, dict[int | str, int])
assert_type(AKeyThreeTuple.m, Dict[int | str | bytes, int])
assert_type(AKeyThreeTuple().m, dict[int | str | bytes, int])
assert_type(AValueFromIntMember.m, Dict[Any, int])
assert_type(AValueFromIntMember().m, dict[Any, int])
assert_type(AKeywordValueFromIntMember.m, Dict[Any, int])
assert_type(AKeywordValueFromIntMember().m, dict[Any, int])
assert_type(AMemberAsKeyAndValue.m, Dict[int, int])
assert_type(AMemberAsKeyAndValue().m, dict[int, int])
20 changes: 20 additions & 0 deletions tests/type_checking/enum_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import assert_type

from atom.api import Atom, Enum

# Enum descriptors hold the member kind on the class while the instance sees the
# specific enum value; helper methods may widen the enum value union.
e = Enum(1, 2)


class A(Atom):
e1 = e
e2 = e("1")
e3 = e.added("1")
e4 = e.removed(2)


assert_type(A.e1, Enum[int])
assert_type(A.e2, Enum[int | str])
assert_type(A.e3, Enum[int | str])
assert_type(A.e4, Enum[int])
Loading
Loading