Skip to content

Commit d6a1b11

Browse files
Merge pull request #211 from nucleic/fixed-tuple
Add FixedTuple member enforcing a given number of items
2 parents 330b8fc + fb808da commit d6a1b11

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+472
-201
lines changed

.coveragerc

-19
This file was deleted.

.flake8

-13
This file was deleted.

.github/workflows/ci.yml

+1-5
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ jobs:
105105
uses: actions/setup-python@v4
106106
with:
107107
python-version: ${{ matrix.python-version }}
108-
cache: 'pip'
109-
cache-dependency-path: 'test_requirements.txt'
110108
- name: Install dependencies
111109
run: |
112110
python -m pip install --upgrade pip
@@ -119,11 +117,9 @@ jobs:
119117
run: |
120118
pip install -e .
121119
- name: Test with pytest
122-
# XXX Disabled warnings check ( -W error) to be able to test on 3.11
123-
# (pyparsing deprecation)
124120
run: |
125121
pip install -r test_requirements.txt
126-
python -X dev -m pytest tests --ignore=tests/type_checking --cov --cov-report xml -v
122+
python -X dev -m pytest tests --ignore=tests/type_checking --cov --cov-report xml -v -W error
127123
- name: Generate C++ coverage reports
128124
if: (github.event_name != 'schedule' && matrix.os != 'windows-latest')
129125
run: |

atom/api.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
#
66
# The full license is in the file LICENSE, distributed with this software.
77
# --------------------------------------------------------------------------------------
8-
"""Module exporting the public interface to atom.
8+
"""Module exporting the public interface to atom."""
99

10-
"""
1110
from .atom import Atom
1211
from .catom import (
1312
CAtom,
@@ -61,7 +60,7 @@
6160
from .set import Set
6261
from .signal import Signal
6362
from .subclass import ForwardSubclass, Subclass
64-
from .tuple import Tuple
63+
from .tuple import FixedTuple, Tuple
6564
from .typed import ForwardTyped, Typed
6665
from .typing_utils import ChangeDict
6766

@@ -120,5 +119,6 @@
120119
"Tuple",
121120
"ForwardTyped",
122121
"Typed",
122+
"FixedTuple",
123123
"ChangeDict",
124124
]

atom/catom.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ class Validate(IntEnum):
537537
Dict = ...
538538
DefaultDict = ...
539539
Enum = ...
540+
FixedTuple = ...
540541
Float = ...
541542
FloatPromote = ...
542543
FloatRange = ...

atom/meta/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# The full license is in the file LICENSE, distributed with this software.
77
# --------------------------------------------------------------------------------------
88
"""Atom metaclass and tools used to create atom subclasses."""
9+
910
from .atom_meta import AtomMeta, MissingMemberWarning, add_member, clone_if_needed
1011
from .member_modifiers import set_default
1112
from .observation import observe

atom/meta/annotation_utils.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ..scalars import Bool, Bytes, Callable as ACallable, Float, Int, Str, Value
1717
from ..set import Set as ASet
1818
from ..subclass import Subclass
19-
from ..tuple import Tuple as ATuple
19+
from ..tuple import FixedTuple, Tuple as ATuple
2020
from ..typed import Typed
2121
from ..typing_utils import extract_types, get_args, is_optional
2222
from .member_modifiers import set_default
@@ -71,10 +71,10 @@ def generate_member_from_type_or_generic(
7171
):
7272
# We can only validate homogeneous tuple so far so we ignore other cases
7373
if t is tuple:
74-
if (...) in parameters or len(set(parameters)) == 1:
74+
if (...) in parameters:
7575
parameters = (parameters[0],)
7676
else:
77-
parameters = ()
77+
m_cls = FixedTuple
7878
parameters = tuple(
7979
generate_member_from_type_or_generic(
8080
t, _NO_DEFAULT, annotate_type_containers - 1

atom/meta/atom_meta.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# The full license is in the file LICENSE, distributed with this software.
77
# --------------------------------------------------------------------------------------
88
"""Metaclass implementing atom members customization."""
9+
910
import copyreg
1011
import warnings
1112
from types import FunctionType

atom/meta/member_modifiers.py

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# The full license is in the file LICENSE, distributed with this software.
77
# --------------------------------------------------------------------------------------
88
"""Custom marker objects used to modify the default settings of a member."""
9+
910
from typing import Any, Optional
1011

1112

@@ -28,3 +29,9 @@ def __init__(self, value: Any) -> None:
2829
def clone(self) -> "set_default":
2930
"""Create a clone of the sentinel."""
3031
return type(self)(self.value)
32+
33+
34+
# XXX add more sentinels here to allow customizing members without using the
35+
# members themselves:
36+
# - tag
37+
#

atom/meta/observation.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# The full license is in the file LICENSE, distributed with this software.
77
# --------------------------------------------------------------------------------------
88
"""Tools to declare static observers in Atom subclasses"""
9+
910
from types import FunctionType
1011
from typing import (
1112
TYPE_CHECKING,

atom/src/behaviors.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*-----------------------------------------------------------------------------
2-
| Copyright (c) 2013-2023, Nucleic Development Team.
2+
| Copyright (c) 2013-2024, Nucleic Development Team.
33
|
44
| Distributed under the terms of the Modified BSD License.
55
|
@@ -132,6 +132,7 @@ enum Mode
132132
Str,
133133
StrPromote,
134134
Tuple,
135+
FixedTuple,
135136
List,
136137
ContainerList,
137138
Set,

atom/src/enumtypes.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ bool init_enumtypes()
267267
add_long( dict_ptr, expand_enum( Str ) );
268268
add_long( dict_ptr, expand_enum( StrPromote ) );
269269
add_long( dict_ptr, expand_enum( Tuple ) );
270+
add_long( dict_ptr, expand_enum( FixedTuple ) );
270271
add_long( dict_ptr, expand_enum( List ) );
271272
add_long( dict_ptr, expand_enum( ContainerList ) );
272273
add_long( dict_ptr, expand_enum( Set ) );

atom/src/validatebehavior.cpp

+70
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ Member::check_context( Validate::Mode mode, PyObject* context )
6767
return false;
6868
}
6969
break;
70+
case Validate::FixedTuple:
71+
{
72+
if( !PyTuple_Check( context ) )
73+
{
74+
cppy::type_error( context, "tuple of types or Members" );
75+
return false;
76+
}
77+
Py_ssize_t len = PyTuple_GET_SIZE( context );
78+
for( Py_ssize_t i = 0; i < len; i++ )
79+
{
80+
PyObject* t = PyTuple_GET_ITEM( context, i );
81+
if( !Member::TypeCheck( t ) )
82+
{
83+
cppy::type_error( context, "tuple of types or Members" );
84+
return false;
85+
}
86+
}
87+
break;
88+
}
7089
case Validate::Dict:
7190
{
7291
if( !PyTuple_Check( context ) )
@@ -463,6 +482,56 @@ tuple_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newval
463482
}
464483

465484

485+
PyObject*
486+
fixed_tuple_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newvalue )
487+
{
488+
if( !PyTuple_Check( newvalue ) )
489+
{
490+
return validate_type_fail( member, atom, newvalue, "tuple" );
491+
}
492+
cppy::ptr tupleptr( cppy::incref( newvalue ) );
493+
494+
// Create a copy in which to store the validated values
495+
Py_ssize_t size = PyTuple_GET_SIZE( newvalue );
496+
cppy::ptr tuplecopy = PyTuple_New( size );
497+
if( !tuplecopy )
498+
{
499+
return 0;
500+
}
501+
502+
// Check the size match the expected size
503+
Py_ssize_t expected_size = PyTuple_GET_SIZE( member->validate_context );
504+
if( size != expected_size )
505+
{
506+
PyErr_Format(
507+
PyExc_TypeError,
508+
"The '%s' member on the '%s' object must be of a '%d-tuple'. "
509+
"Got tuple of length %d instead",
510+
PyUnicode_AsUTF8( member->name ),
511+
Py_TYPE( pyobject_cast( atom ) )->tp_name,
512+
expected_size,
513+
size
514+
);
515+
return 0;
516+
}
517+
518+
// Validate each single item
519+
for( Py_ssize_t i = 0; i < size; ++i )
520+
{
521+
Member* item_member = member_cast( PyTuple_GET_ITEM( member->validate_context, i ) );
522+
cppy::ptr item( cppy::incref( PyTuple_GET_ITEM( tupleptr.get(), i ) ) );
523+
cppy::ptr valid_item( item_member->full_validate( atom, Py_None, item.get() ) );
524+
if( !valid_item )
525+
{
526+
return 0;
527+
}
528+
PyTuple_SET_ITEM( tuplecopy.get(), i, valid_item.release() );
529+
}
530+
tupleptr = tuplecopy;
531+
return tupleptr.release();
532+
}
533+
534+
466535
template<typename ListFactory> PyObject*
467536
common_list_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newvalue )
468537
{
@@ -912,6 +981,7 @@ handlers[] = {
912981
str_handler,
913982
str_promote_handler,
914983
tuple_handler,
984+
fixed_tuple_handler,
915985
list_handler,
916986
container_list_handler,
917987
set_handler,

atom/tuple.py

+79-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# --------------------------------------------------------------------------------------
2-
# Copyright (c) 2013-2023, Nucleic Development Team.
2+
# Copyright (c) 2013-2024, Nucleic Development Team.
33
#
44
# Distributed under the terms of the Modified BSD License.
55
#
66
# The full license is in the file LICENSE, distributed with this software.
77
# --------------------------------------------------------------------------------------
8+
from typing import Tuple as TTuple
9+
810
from .catom import DefaultValue, Member, Validate
911
from .instance import Instance
1012
from .typing_utils import extract_types, is_optional
@@ -78,3 +80,79 @@ def clone(self):
7880
else:
7981
clone.item = None
8082
return clone
83+
84+
85+
class FixedTuple(Member):
86+
"""A member which allows tuple values with a fixed number of items.
87+
88+
Items are always validated and can be of different types.
89+
Assignment will create a copy of the original tuple before validating the
90+
items, since validation may change the item values.
91+
92+
"""
93+
94+
#: Members used to validate each element of the tuple.
95+
items: TTuple[Member, ...]
96+
97+
__slots__ = ("items",)
98+
99+
def __init__(self, *items, default=None):
100+
"""Initialize a Tuple.
101+
102+
Parameters
103+
----------
104+
items : Member, type, or tuple of types
105+
A member to use for validating the types of items allowed in
106+
the tuple. This can also be a type object or a tuple of types,
107+
in which case it will be wrapped with an Instance member.
108+
109+
default : tuple, optional
110+
The default tuple of values.
111+
112+
"""
113+
mitems = []
114+
for i in items:
115+
if not isinstance(i, Member):
116+
opt, types = is_optional(extract_types(i))
117+
i = Instance(types, optional=opt)
118+
mitems.append(i)
119+
120+
self.items = mitems
121+
122+
if default is None:
123+
self.set_default_value_mode(DefaultValue.NonOptional, None)
124+
else:
125+
self.set_default_value_mode(DefaultValue.Static, default)
126+
self.set_validate_mode(Validate.FixedTuple, tuple(mitems))
127+
128+
def set_name(self, name):
129+
"""Set the name of the member.
130+
131+
This method ensures that the item member name is also updated.
132+
133+
"""
134+
super().set_name(name)
135+
for i, item in enumerate(self.items):
136+
item.set_name(name + f"|item_{i}")
137+
138+
def set_index(self, index):
139+
"""Assign the index to this member.
140+
141+
This method ensures that the item member index is also updated.
142+
143+
"""
144+
super().set_index(index)
145+
for item in self.items:
146+
item.set_index(index)
147+
148+
def clone(self):
149+
"""Create a clone of the tuple.
150+
151+
This will clone the internal tuple item if one is in use.
152+
153+
"""
154+
clone = super().clone()
155+
clone.items = items_clone = tuple(i.clone() for i in self.items)
156+
mode, _ = self.validate_mode
157+
clone.set_validate_mode(mode, items_clone)
158+
return clone

0 commit comments

Comments
 (0)