Skip to content

Commit

Permalink
Merge pull request #211 from nucleic/fixed-tuple
Browse files Browse the repository at this point in the history
Add FixedTuple member enforcing a given number of items
  • Loading branch information
MatthieuDartiailh authored Mar 25, 2024
2 parents 330b8fc + fb808da commit d6a1b11
Show file tree
Hide file tree
Showing 67 changed files with 472 additions and 201 deletions.
19 changes: 0 additions & 19 deletions .coveragerc

This file was deleted.

13 changes: 0 additions & 13 deletions .flake8

This file was deleted.

6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: 'test_requirements.txt'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -119,11 +117,9 @@ jobs:
run: |
pip install -e .
- name: Test with pytest
# XXX Disabled warnings check ( -W error) to be able to test on 3.11
# (pyparsing deprecation)
run: |
pip install -r test_requirements.txt
python -X dev -m pytest tests --ignore=tests/type_checking --cov --cov-report xml -v
python -X dev -m pytest tests --ignore=tests/type_checking --cov --cov-report xml -v -W error
- name: Generate C++ coverage reports
if: (github.event_name != 'schedule' && matrix.os != 'windows-latest')
run: |
Expand Down
6 changes: 3 additions & 3 deletions atom/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
#
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
"""Module exporting the public interface to atom.
"""Module exporting the public interface to atom."""

"""
from .atom import Atom
from .catom import (
CAtom,
Expand Down Expand Up @@ -61,7 +60,7 @@
from .set import Set
from .signal import Signal
from .subclass import ForwardSubclass, Subclass
from .tuple import Tuple
from .tuple import FixedTuple, Tuple
from .typed import ForwardTyped, Typed
from .typing_utils import ChangeDict

Expand Down Expand Up @@ -120,5 +119,6 @@
"Tuple",
"ForwardTyped",
"Typed",
"FixedTuple",
"ChangeDict",
]
1 change: 1 addition & 0 deletions atom/catom.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ class Validate(IntEnum):
Dict = ...
DefaultDict = ...
Enum = ...
FixedTuple = ...
Float = ...
FloatPromote = ...
FloatRange = ...
Expand Down
1 change: 1 addition & 0 deletions atom/meta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
"""Atom metaclass and tools used to create atom subclasses."""

from .atom_meta import AtomMeta, MissingMemberWarning, add_member, clone_if_needed
from .member_modifiers import set_default
from .observation import observe
Expand Down
6 changes: 3 additions & 3 deletions atom/meta/annotation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ..scalars import Bool, Bytes, Callable as ACallable, Float, Int, Str, Value
from ..set import Set as ASet
from ..subclass import Subclass
from ..tuple import Tuple as ATuple
from ..tuple import FixedTuple, Tuple as ATuple
from ..typed import Typed
from ..typing_utils import extract_types, get_args, is_optional
from .member_modifiers import set_default
Expand Down Expand Up @@ -71,10 +71,10 @@ def generate_member_from_type_or_generic(
):
# We can only validate homogeneous tuple so far so we ignore other cases
if t is tuple:
if (...) in parameters or len(set(parameters)) == 1:
if (...) in parameters:
parameters = (parameters[0],)
else:
parameters = ()
m_cls = FixedTuple
parameters = tuple(
generate_member_from_type_or_generic(
t, _NO_DEFAULT, annotate_type_containers - 1
Expand Down
1 change: 1 addition & 0 deletions atom/meta/atom_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
"""Metaclass implementing atom members customization."""

import copyreg
import warnings
from types import FunctionType
Expand Down
7 changes: 7 additions & 0 deletions atom/meta/member_modifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
"""Custom marker objects used to modify the default settings of a member."""

from typing import Any, Optional


Expand All @@ -28,3 +29,9 @@ def __init__(self, value: Any) -> None:
def clone(self) -> "set_default":
"""Create a clone of the sentinel."""
return type(self)(self.value)


# XXX add more sentinels here to allow customizing members without using the
# members themselves:
# - tag
#
1 change: 1 addition & 0 deletions atom/meta/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
"""Tools to declare static observers in Atom subclasses"""

from types import FunctionType
from typing import (
TYPE_CHECKING,
Expand Down
3 changes: 2 additions & 1 deletion atom/src/behaviors.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2023, Nucleic Development Team.
| Copyright (c) 2013-2024, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
Expand Down Expand Up @@ -132,6 +132,7 @@ enum Mode
Str,
StrPromote,
Tuple,
FixedTuple,
List,
ContainerList,
Set,
Expand Down
1 change: 1 addition & 0 deletions atom/src/enumtypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ bool init_enumtypes()
add_long( dict_ptr, expand_enum( Str ) );
add_long( dict_ptr, expand_enum( StrPromote ) );
add_long( dict_ptr, expand_enum( Tuple ) );
add_long( dict_ptr, expand_enum( FixedTuple ) );
add_long( dict_ptr, expand_enum( List ) );
add_long( dict_ptr, expand_enum( ContainerList ) );
add_long( dict_ptr, expand_enum( Set ) );
Expand Down
70 changes: 70 additions & 0 deletions atom/src/validatebehavior.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ Member::check_context( Validate::Mode mode, PyObject* context )
return false;
}
break;
case Validate::FixedTuple:
{
if( !PyTuple_Check( context ) )
{
cppy::type_error( context, "tuple of types or Members" );
return false;
}
Py_ssize_t len = PyTuple_GET_SIZE( context );
for( Py_ssize_t i = 0; i < len; i++ )
{
PyObject* t = PyTuple_GET_ITEM( context, i );
if( !Member::TypeCheck( t ) )
{
cppy::type_error( context, "tuple of types or Members" );
return false;
}
}
break;
}
case Validate::Dict:
{
if( !PyTuple_Check( context ) )
Expand Down Expand Up @@ -463,6 +482,56 @@ tuple_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newval
}


PyObject*
fixed_tuple_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newvalue )
{
if( !PyTuple_Check( newvalue ) )
{
return validate_type_fail( member, atom, newvalue, "tuple" );
}
cppy::ptr tupleptr( cppy::incref( newvalue ) );

// Create a copy in which to store the validated values
Py_ssize_t size = PyTuple_GET_SIZE( newvalue );
cppy::ptr tuplecopy = PyTuple_New( size );
if( !tuplecopy )
{
return 0;
}

// Check the size match the expected size
Py_ssize_t expected_size = PyTuple_GET_SIZE( member->validate_context );
if( size != expected_size )
{
PyErr_Format(
PyExc_TypeError,
"The '%s' member on the '%s' object must be of a '%d-tuple'. "
"Got tuple of length %d instead",
PyUnicode_AsUTF8( member->name ),
Py_TYPE( pyobject_cast( atom ) )->tp_name,
expected_size,
size
);
return 0;
}

// Validate each single item
for( Py_ssize_t i = 0; i < size; ++i )
{
Member* item_member = member_cast( PyTuple_GET_ITEM( member->validate_context, i ) );
cppy::ptr item( cppy::incref( PyTuple_GET_ITEM( tupleptr.get(), i ) ) );
cppy::ptr valid_item( item_member->full_validate( atom, Py_None, item.get() ) );
if( !valid_item )
{
return 0;
}
PyTuple_SET_ITEM( tuplecopy.get(), i, valid_item.release() );
}
tupleptr = tuplecopy;
return tupleptr.release();
}


template<typename ListFactory> PyObject*
common_list_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newvalue )
{
Expand Down Expand Up @@ -912,6 +981,7 @@ handlers[] = {
str_handler,
str_promote_handler,
tuple_handler,
fixed_tuple_handler,
list_handler,
container_list_handler,
set_handler,
Expand Down
80 changes: 79 additions & 1 deletion atom/tuple.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# --------------------------------------------------------------------------------------
# Copyright (c) 2013-2023, Nucleic Development Team.
# Copyright (c) 2013-2024, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
from typing import Tuple as TTuple

from .catom import DefaultValue, Member, Validate
from .instance import Instance
from .typing_utils import extract_types, is_optional
Expand Down Expand Up @@ -78,3 +80,79 @@ def clone(self):
else:
clone.item = None
return clone


class FixedTuple(Member):
"""A member which allows tuple values with a fixed number of items.
Items are always validated and can be of different types.
Assignment will create a copy of the original tuple before validating the
items, since validation may change the item values.
"""

#: Members used to validate each element of the tuple.
items: TTuple[Member, ...]

__slots__ = ("items",)

def __init__(self, *items, default=None):
"""Initialize a Tuple.
Parameters
----------
items : Member, type, or tuple of types
A member to use for validating the types of items allowed in
the tuple. This can also be a type object or a tuple of types,
in which case it will be wrapped with an Instance member.
default : tuple, optional
The default tuple of values.
"""
mitems = []
for i in items:
if not isinstance(i, Member):
opt, types = is_optional(extract_types(i))
i = Instance(types, optional=opt)
mitems.append(i)

self.items = mitems

if default is None:
self.set_default_value_mode(DefaultValue.NonOptional, None)
else:
self.set_default_value_mode(DefaultValue.Static, default)
self.set_validate_mode(Validate.FixedTuple, tuple(mitems))

def set_name(self, name):
"""Set the name of the member.
This method ensures that the item member name is also updated.
"""
super().set_name(name)
for i, item in enumerate(self.items):
item.set_name(name + f"|item_{i}")

def set_index(self, index):
"""Assign the index to this member.
This method ensures that the item member index is also updated.
"""
super().set_index(index)
for item in self.items:
item.set_index(index)

def clone(self):
"""Create a clone of the tuple.
This will clone the internal tuple item if one is in use.
"""
clone = super().clone()
clone.items = items_clone = tuple(i.clone() for i in self.items)
mode, _ = self.validate_mode
clone.set_validate_mode(mode, items_clone)
return clone
Loading

0 comments on commit d6a1b11

Please sign in to comment.