Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stubs for Array, ArrayOrNone, and CArray #1682

Merged
merged 7 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions traits-stubs/traits-stubs/api.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,9 @@ from .trait_handlers import (
TraitPrefixMap as TraitPrefixMap,
TraitCompound as TraitCompound,
)

from .trait_numeric import (
Array as Array,
ArrayOrNone as ArrayOrNone,
CArray as CArray,
)
68 changes: 68 additions & 0 deletions traits-stubs/traits-stubs/trait_numeric.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

from typing import Any, List, Optional, Tuple, Type, Union

import numpy as np

from .trait_type import _TraitType

# Things that are allowed as individual shape elements in the 'shape'
# tuple or list.
_ShapeElement = Union[None, int, Tuple[int, Union[None, int]]]

# Type for the shape parameter.
_Shape = Union[Tuple[_ShapeElement, ...], List[_ShapeElement]]

# The "Array" trait type is not as permissive as NumPy's asarray: it
# accepts only NumPy arrays, lists and tuples.
_ArrayLike = Union[List[Any], Tuple[Any, ...], np.ndarray[Any, Any]]

# Synonym for the "stores" type of the trait.
_Array = np.ndarray[Any, Any]

# Things that are accepted as dtypes. This doesn't attempt to cover
# all legal possibilities - only those that are common.
_DTypeLike = Union[np.dtype[Any], Type[Any], str]

class Array(_TraitType[_ArrayLike, _Array]):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need AbstractArray as well?

A quick search and I can't see any uses of it, but it is nominally public.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbstractArray isn't exposed in traits.api, so I'm happy leaving it out of the stubs for now.

def __init__(
self,
dtype: Optional[_DTypeLike] = ...,
shape: Optional[_Shape] = ...,
value: Optional[_ArrayLike] = ...,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this array-like, or do we expect an actual array here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array-like: e.g., you can provide a list:

>>> from traits.api import HasTraits, Array
>>> class A(HasTraits):
...     foo = Array(shape=(None,), value=[1, 2, 3])
... 
>>> a = A()
>>> a.foo
array([1, 2, 3])

*,
casting: str = ...,
**metadata: Any,
) -> None: ...

class ArrayOrNone(
_TraitType[Optional[_ArrayLike], Optional[_Array]]
):
def __init__(
self,
dtype: Optional[_DTypeLike] = ...,
shape: Optional[_Shape] = ...,
value: Optional[_ArrayLike] = ...,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: is this _Array or _ArrayLike?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer. :-) _ArrayLike is the right thing, I think.

*,
casting: str = ...,
**metadata: Any,
) -> None: ...

class CArray(_TraitType[_ArrayLike, _Array]):
def __init__(
self,
dtype: Optional[_DTypeLike] = ...,
shape: Optional[_Shape] = ...,
value: Optional[_ArrayLike] = ...,
*,
casting: str = ...,
**metadata: Any,
) -> None: ...
28 changes: 28 additions & 0 deletions traits-stubs/traits_stubs_tests/numpy_examples/Array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

import numpy as np

from traits.api import Array, ArrayOrNone, HasTraits


class HasArrayTraits(HasTraits):
spectrum = Array(shape=(None,), dtype=np.float64)
maybe_image = ArrayOrNone(shape=(None, None, 3), dtype=np.float64)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a test for CArray?

Similarly, it might be good to test the dimension ranges (eg. `maybe_rgba = Array(shape=(None, None, (3,4)), ...)

Also, is it worth testing for invalid trait parameters (eg. passing a default value of None to Array)?

Some of this may be overkill if we expect that things will Just Work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably test CArray. I'm a bit grumpy with CArray, because at this point it's actually identical in functionality to Array, so I'm thinking that we might want to deprecate it.

I can flesh out the tests a bit more to test invalid parameters.

As an aside, I think at some point we're going to need a better framework for these tests; right now, there's a lot of interaction between tests that should be independent of one another.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eg. passing a default value of None to Array

That particular example is troublesome: as far as I can tell, in general there's no way to type hint a function parameter that has a default value of None, but for which the intended API is that None is never passed explicitly.

I've added a few more tests, including tests for bad declarations.



obj = HasArrayTraits()
obj.spectrum = np.array([2, 3, 4], dtype=np.float64)
obj.spectrum = "not an array" # E: assignment
obj.spectrum = None # E: assignment

obj.maybe_image = None
obj.maybe_image = np.zeros((5, 5, 3))
obj.maybe_image = 2.3 # E: assignment
23 changes: 22 additions & 1 deletion traits-stubs/traits_stubs_tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
from traits.testing.optional_dependencies import (
pkg_resources,
requires_mypy,
requires_numpy_testing,
requires_pkg_resources,
)
from traits_stubs_tests.util import MypyAssertions


@requires_pkg_resources
@requires_mypy
class TestAnnotations(TestCase, MypyAssertions):
@requires_pkg_resources
def test_all(self, filename_suffix=""):
""" Run mypy for all files contained in traits_stubs_tests/examples
directory.
Expand All @@ -41,3 +42,23 @@ def test_all(self, filename_suffix=""):
for file_path in examples_dir.glob("*{}.py".format(filename_suffix)):
with self.subTest(file_path=file_path):
self.assertRaisesMypyError(file_path)

@requires_numpy_testing
def test_numpy_examples(self):
""" Run mypy for files contained in traits_stubs_tests/numpy_examples
directory.

Lines with expected errors are marked inside these files.
Any mismatch will raise an assertion error.

Parameters
----------
filename_suffix: str
Optional filename suffix filter.
"""
examples_dir = Path(pkg_resources.resource_filename(
'traits_stubs_tests', 'numpy_examples'))

for file_path in examples_dir.glob("*.py"):
with self.subTest(file_path=file_path):
self.assertRaisesMypyError(file_path)
7 changes: 7 additions & 0 deletions traits/testing/optional_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ def optional_import(name):
numpy = optional_import("numpy")
requires_numpy = unittest.skipIf(numpy is None, "NumPy not available")

if numpy is not None:
mdickinson marked this conversation as resolved.
Show resolved Hide resolved
numpy_testing = optional_import("numpy.testing")
else:
numpy_testing = None
requires_numpy_testing = unittest.skipIf(
numpy_testing is None, "numpy.testing not available")

pkg_resources = optional_import("pkg_resources")
requires_pkg_resources = unittest.skipIf(
pkg_resources is None, "pkg_resources not available"
Expand Down