diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5abda71b..03e93c1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,7 +110,7 @@ jobs: - name: Run tests run: | - pytest --pyargs orix --runslow --reruns 2 -n 2 --cov=orix + pytest --pyargs orix --slow --reruns 2 -n 2 --cov=orix - name: Generate line coverage if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/doc/conf.py b/doc/conf.py index 2b81c2d3..9dafd02c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,3 +1,22 @@ +# +# Copyright 2019-2025 the orix developers +# +# This file is part of orix. +# +# orix is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# orix is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with orix. If not, see . +# + # Configuration file for the Sphinx documentation app. # See the documentation for a full list of configuration options: # https://www.sphinx-doc.org/en/master/usage/configuration.html @@ -64,9 +83,10 @@ "numpydoc": ("https://numpydoc.readthedocs.io/en/latest", None), "pooch": ("https://www.fatiando.org/pooch/latest", None), "pytest": ("https://docs.pytest.org/en/stable", None), + "pytest-xdist": ("https://pytest-xdist.readthedocs.io/en/stable/", None), "python": ("https://docs.python.org/3", None), "pyxem": ("https://pyxem.readthedocs.io/en/latest", None), - "readthedocs": ("https://docs.readthedocs.io/en/stable", None), + "readthedocs": ("https://docs.readthedocs.com/platform/stable/", None), "scipy": ("https://docs.scipy.org/doc/scipy", None), "sklearn": ("https://scikit-learn.org/stable", None), "sphinx": ("https://www.sphinx-doc.org/en/master", None), diff --git a/doc/dev/running_writing_tests.rst b/doc/dev/running_writing_tests.rst index ab205cf4..da832cb8 100644 --- a/doc/dev/running_writing_tests.rst +++ b/doc/dev/running_writing_tests.rst @@ -3,11 +3,12 @@ Run and write tests All functionality in orix is tested with :doc:`pytest `. The tests reside in a ``tests`` module. -Tests are short methods that call functions in ``orix`` and compare resulting output -values with known answers. +Tests are short methods that call functions in orix and compare resulting output values +with known answers. + Install necessary dependencies to run the tests:: - pip install --editable ".[tests]" + pip install -e ".[tests]" Some useful :doc:`fixtures ` are available in the ``conftest.py`` file. @@ -22,29 +23,28 @@ Some useful :doc:`fixtures ` are available in the To run the tests:: - pytest --cov --pyargs orix -n auto + pytest --cov --pyargs orix -n auto -The ``-n auto`` is an optional flag to enable parallelized testing. -The ``--cov`` flag makes :doc:`coverage.py ` print a nice report. -For an even nicer presentation, you can use ``coverage.py`` directly:: +The ``-n auto`` is an optional flag to enable parallelized testing with +:doc:`pytest-xdist `. +We aim to cover all lines when all :ref:`dependencies` are installed. +The ``--cov`` flag makes :doc:`coverage.py ` print a nice coverage +report. +For an even nicer presentation, you can use coverage.py directly:: - coverage html + coverage html Coverage can then be inspected in the browser by opening ``htmlcov/index.html``. -We strive for 100% test coverage of lines when all dependencies are installed. - -If you have a test that takes a long time to run, you can mark it to skip it from running by default - -.. code-block:: Python +If a test takes a long time to run, you can mark it to skip it from running by default:: @pytest.mark.slow def test_slow_function(): - pass + ... -Then you can run the tests with the ``--runslow`` option to skip slow tests:: +To run tests marked as slow, add the flag when running pytest:: - pytest --runslow + pytest --slow Docstring examples are tested with :doc:`pytest ` as well. :mod:`numpy` and :mod:`matplotlib.pyplot` should not be imported in examples as they are diff --git a/orix/crystal_map/phase_list.py b/orix/crystal_map/phase_list.py index ff99ee89..ce7e3b91 100644 --- a/orix/crystal_map/phase_list.py +++ b/orix/crystal_map/phase_list.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from __future__ import annotations @@ -37,7 +39,8 @@ _groups, get_point_group, ) -from orix.vector import Miller, Vector3d +from orix.vector.miller import Miller +from orix.vector.vector3d import Vector3d # All named Matplotlib colors (tableau and xkcd already lower case hex) ALL_COLORS = mcolors.TABLEAU_COLORS @@ -366,18 +369,26 @@ def deepcopy(self) -> Phase: return copy.deepcopy(self) def expand_asymmetric_unit(self) -> Phase: - """Return new instance with all symmetrically equivalent atoms. + """Return a new phase with all symmetrically equivalent atoms. + + Returns + ------- + expanded_phase + New phase with the a :attr:`structure` with the unit cell + filled with symmetrically equivalent atoms. Examples -------- + >>> from diffpy.structure import Atom, Lattice, Structure + >>> import orix.crystal_map as ocm >>> atoms = [Atom("Si", xyz=(0, 0, 1))] >>> lattice = Lattice(4.04, 4.04, 4.04, 90, 90, 90) >>> structure = Structure(atoms = atoms,lattice=lattice) - >>> phase = Phase(structure=structure, space_group=227) + >>> phase = ocm.Phase(structure=structure, space_group=227) >>> phase.structure [Si 0.000000 0.000000 1.000000 1.0000] - >>> expanded = phase.expand_asymmetric_unit() - >>> expanded.structure + >>> expanded_phase = phase.expand_asymmetric_unit() + >>> expanded_phase.structure [Si 0.000000 0.000000 0.000000 1.0000, Si 0.000000 0.500000 0.500000 1.0000, Si 0.500000 0.500000 0.000000 1.0000, @@ -411,9 +422,10 @@ def expand_asymmetric_unit(self) -> Phase: diffpy_structure.append(new_atom) # This handles conversion back to correct alignment - out = Phase(self) - out.structure = diffpy_structure - return out + expanded_phase = self.__class__(self) + expanded_phase.structure = diffpy_structure + + return expanded_phase class PhaseList: diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 9e0b260d..b0b67730 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,15 +10,16 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from __future__ import annotations -from typing import Any, Optional, Tuple, Union +from typing import Any import warnings import dask.array as da @@ -29,7 +31,9 @@ from orix._base import Object3d from orix.constants import installed from orix.quaternion import _conversions -from orix.vector import AxAngle, Homochoric, Miller, Rodrigues, Vector3d +from orix.vector.miller import Miller +from orix.vector.neo_euler import AxAngle, Homochoric, Rodrigues +from orix.vector.vector3d import Vector3d class Quaternion(Object3d): @@ -193,9 +197,7 @@ def conj(self) -> Quaternion: def __invert__(self) -> Quaternion: return self.__class__(self.conj.data / (self.norm**2)[..., np.newaxis]) - def __mul__( - self, other: Union[Quaternion, Vector3d] - ) -> Union[Quaternion, Vector3d]: + def __mul__(self, other: Quaternion | Vector3d) -> Quaternion | Vector3d: if isinstance(other, Quaternion): if installed["numpy-quaternion"]: import quaternion @@ -231,7 +233,7 @@ def __mul__( def __neg__(self) -> Quaternion: return self.__class__(-self.data) - def __eq__(self, other: Union[Any, Quaternion]) -> bool: + def __eq__(self, other: Any | Quaternion) -> bool: """Check if quaternions have equal shapes and components.""" if ( isinstance(other, Quaternion) @@ -247,8 +249,8 @@ def __eq__(self, other: Union[Any, Quaternion]) -> bool: @classmethod def from_axes_angles( cls, - axes: Union[np.ndarray, Vector3d, tuple, list], - angles: Union[np.ndarray, tuple, list, float], + axes: np.ndarray | Vector3d | tuple | list, + angles: np.ndarray | tuple | list | float, degrees: bool = False, ) -> Quaternion: r"""Create unit quaternions from axis-angle pairs @@ -299,8 +301,7 @@ def from_axes_angles( @classmethod def from_homochoric( - cls, - ho: Union[Vector3d, Homochoric, np.ndarray, tuple, list], + cls, ho: Vector3d | Homochoric | np.ndarray | tuple | list ) -> Quaternion: r"""Create unit quaternions from homochoric vectors :math:`\mathbf{h}` :cite:`rowenhorst2015consistent`. @@ -346,8 +347,8 @@ def from_homochoric( @classmethod def from_rodrigues( cls, - ro: Union[np.ndarray, Vector3d, tuple, list], - angles: Union[np.ndarray, tuple, list, float, None] = None, + ro: np.ndarray | Vector3d | tuple | list, + angles: np.ndarray | tuple | list | float | None = None, ) -> Quaternion: r"""Create unit quaternions from three-component Rodrigues vectors :math:`\hat{\mathbf{n}}` or four-component @@ -448,7 +449,7 @@ def from_rodrigues( @classmethod def from_euler( cls, - euler: Union[np.ndarray, tuple, list], + euler: np.ndarray | tuple | list, direction: str = "lab2crystal", degrees: bool = False, ) -> Quaternion: @@ -505,7 +506,7 @@ def from_euler( return Q @classmethod - def from_matrix(cls, matrix: Union[np.ndarray, tuple, list]) -> Quaternion: + def from_matrix(cls, matrix: np.ndarray | tuple | list) -> Quaternion: """Create unit quaternions from orientation matrices :cite:`rowenhorst2015consistent`. @@ -552,9 +553,13 @@ def from_scipy_rotation(cls, rotation: SciPyRotation) -> Quaternion: Returns ------- - quaternion + Q Quaternions. + See Also + -------- + to_scipy_rotation + Notes ----- The SciPy rotation is inverted to be consistent with the orix @@ -600,17 +605,17 @@ def from_scipy_rotation(cls, rotation: SciPyRotation) -> Quaternion: @classmethod def from_align_vectors( cls, - other: Union[Vector3d, tuple, list], - initial: Union[Vector3d, tuple, list], - weights: Optional[np.ndarray] = None, + other: Vector3d | tuple | list, + initial: Vector3d | tuple | list, + weights: np.ndarray | None = None, return_rmsd: bool = False, return_sensitivity: bool = False, - ) -> Union[ - Quaternion, - Tuple[Quaternion, float], - Tuple[Quaternion, np.ndarray], - Tuple[Quaternion, float, np.ndarray], - ]: + ) -> ( + Quaternion + | tuple[Quaternion, float] + | tuple[Quaternion, np.ndarray] + | tuple[Quaternion, float, np.ndarray] + ): """Estimate a quaternion to optimally align two sets of vectors. This method wraps @@ -739,7 +744,7 @@ def triple_cross(cls, q1: Quaternion, q2: Quaternion, q3: Quaternion) -> Quatern return Q @classmethod - def identity(cls, shape: Union[int, tuple] = (1,)) -> Quaternion: + def identity(cls, shape: int | tuple = (1,)) -> Quaternion: """Create identity quaternions. Parameters @@ -836,7 +841,7 @@ def to_axes_angles(self) -> AxAngle: ax = AxAngle(axes * angles) return ax - def to_rodrigues(self, frank: bool = False) -> Union[Rodrigues, np.ndarray]: + def to_rodrigues(self, frank: bool = False) -> Rodrigues | np.ndarray: r"""Return the unit quaternions as Rodrigues or Rodrigues-Frank vectors :cite:`rowenhorst2015consistent`. @@ -946,45 +951,44 @@ def to_homochoric(self) -> Homochoric: return ho def to_scipy_rotation(self) -> SciPyRotation: - r"""Return the unit quaternions as - :class:`scipy.spatial.transform.Rotation` objects used in scipy's - spatial module. + r"""Return unit quaternions as a SciPy rotation. Returns ------- - SciPy_Rotation - a Rotation object generated from the unit quaternion data - (i.e, unaffected by symmetry, phase, or length). + scipy_rotation + A SciPy rotation (flattened) given by the unit quaternions + without considering any symmetry. + + See Also + -------- + from_scipy_rotation Notes ----- - SciPy by default uses the Active rotation convention along with the - vector-scalar quaternion definition, as opposed to ORIX's passive, - scalar-vector convention. Thus, the following quaternion in orix: - :math: `q_{orix} = [q_0, q_1, q_2, q_3]` - represents the same operations as the following quaternion in scipy: - :math: `q_{SciPy} = [-q_1, -q_2, -q_3, q_0]` - - See the function description for Quaternion.from_scipy_rotation - for an example of how these differing parameterizations still produce - identical rotation operations. - - Additionally, note that Orix enforces :math: `q_0 >= 0` whereas - SciPy does not. Thus, the operation - - >>> Quaternion.from_scipy_rotation(r).to_scipy_rotation.as_quat() - - will produce an identical rotation operation, but not - necessarily an idential quaternion. Look up "quaternion double cover" - for more information on why this occurs. - - Finally, ORIX supports N-dimensional arrays, whereas SciPy - currently supports only 1-dimensional vectors. Thus, this function - will also flatten arrays when converting to SciPy Rotations. + SciPy by default uses the active rotation interpretation along + with the vector-scalar quaternion definition, as opposed to + orix's passive one, scalar-vector interpretation. Thus, the + following quaternion in orix, + :math:`Q_{orix} = [q_0, q_1, q_2, q_3]` represents the same + transformation as the following quaternion in SciPy: + :math:`Q_{SciPy} = [-q_1, -q_2, -q_3, q_0]` + + See the function description for :meth:`from_scipy_rotation` for + an example of how these differing parameterizations still + produce identical transformations. + + Additionally, note that orix enforces :math:`Q_0 \geq 0` whereas + SciPy does not. Thus, the operation:: + + Quaternion.from_scipy_rotation(r).to_scipy_rotation.as_quat() + + will produce an identical transformation, but not necessarily an + idential quaternion. Look up "quaternion double cover" for more + information on why this occurs. """ if self.ndim > 1: warnings.warn( - "\n {} dimension greater than 1. ".format(self.__class__.__name__) + f"\n {self.__class__.__name__} dimension greater than 1. " + "Flattening into a 1-dimensional vector" ) self = self.flatten() @@ -1075,11 +1079,11 @@ def mean(self) -> Quaternion: def outer( self, - other: Union[Quaternion, Vector3d], + other: Quaternion | Vector3d, lazy: bool = False, chunk_size: int = 20, progressbar: bool = True, - ) -> Union[Quaternion, Vector3d]: + ) -> Quaternion | Vector3d: """Return the outer products of the quaternions and the other quaternions or vectors. @@ -1171,7 +1175,7 @@ def inv(self) -> Quaternion: # -------------------- Other private methods --------------------- # def _outer_dask( - self, other: Union[Quaternion, Vector3d], chunk_size: int = 20 + self, other: Quaternion | Vector3d, chunk_size: int = 20 ) -> da.Array: """Compute the product of every quaternion in this instance to every quaternion or vector in another instance, returned as a diff --git a/orix/quaternion/rotation.py b/orix/quaternion/rotation.py index 64ce43e8..811f79a0 100644 --- a/orix/quaternion/rotation.py +++ b/orix/quaternion/rotation.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,23 +10,24 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from __future__ import annotations -from typing import Any, Tuple, Union +from typing import Any import dask.array as da from dask.diagnostics import ProgressBar import numpy as np from scipy.special import hyp0f1 -from orix.quaternion import Quaternion -from orix.vector import Vector3d +from orix.quaternion.quaternion import Quaternion +from orix.vector.vector3d import Vector3d class Rotation(Quaternion): @@ -73,7 +75,7 @@ class Rotation(Quaternion): True """ - def __init__(self, data: Union[np.ndarray, Rotation, list, tuple]): + def __init__(self, data: np.ndarray | Rotation | list | tuple) -> None: super().__init__(data) self._data = np.concatenate((self.data, np.zeros(self.shape + (1,))), axis=-1) if isinstance(data, Rotation): @@ -104,8 +106,8 @@ def antipodal(self) -> Rotation: # ------------------------ Dunder methods ------------------------ # def __mul__( - self, other: Union[Rotation, Quaternion, Vector3d, np.ndarray, int, list] - ): + self, other: Rotation | Quaternion | Vector3d | np.ndarray | int | list + ) -> Rotation | Quaternion | Vector3d: # Combine rotations self * other as first other, then self if isinstance(other, Rotation): Q = Quaternion(self) * Quaternion(other) @@ -146,7 +148,7 @@ def __invert__(self) -> Rotation: R.improper = self.improper return R - def __eq__(self, other: Union[Any, Rotation]) -> bool: + def __eq__(self, other: Any | Rotation) -> bool: """Check if the rotations have equal shapes and values.""" if ( isinstance(other, Rotation) @@ -163,9 +165,9 @@ def __eq__(self, other: Union[Any, Rotation]) -> bool: @classmethod def random_vonmises( cls, - shape: Union[int, tuple] = (1,), + shape: int | tuple = (1,), alpha: float = 1.0, - reference: Union[list, tuple, Rotation] = (1, 0, 0, 0), + reference: Rotation | list | tuple = (1, 0, 0, 0), ) -> Rotation: """Return random rotations with a simplified Von Mises-Fisher distribution. @@ -206,11 +208,9 @@ def unique( return_index: bool = False, return_inverse: bool = False, antipodal: bool = True, - ) -> Union[ - Rotation, - Tuple[Rotation, np.ndarray], - Tuple[Rotation, np.ndarray, np.ndarray], - ]: + ) -> ( + Rotation | tuple[Rotation, np.ndarray] | tuple[Rotation, np.ndarray, np.ndarray] + ): """Return the unique rotations from these rotations. Two rotations are not unique if they have the same propriety @@ -343,11 +343,11 @@ def angle_with_outer(self, other: Rotation, degrees: bool = False) -> np.ndarray def outer( self, - other: Union[Rotation, Vector3d], + other: Rotation | Vector3d, lazy: bool = False, chunk_size: int = 20, progressbar: bool = True, - ) -> Union[Rotation, Vector3d]: + ) -> Rotation | Vector3d: """Return the outer rotation products of the rotations and the other rotations or vectors. diff --git a/orix/quaternion/symmetry.py b/orix/quaternion/symmetry.py index ec3061ef..417f04ad 100644 --- a/orix/quaternion/symmetry.py +++ b/orix/quaternion/symmetry.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from __future__ import annotations @@ -24,11 +26,11 @@ import numpy as np from orix.quaternion.rotation import Rotation -from orix.vector import Vector3d +from orix.vector.vector3d import Vector3d if TYPE_CHECKING: # pragma: no cover - from orix.quaternion import Orientation - from orix.vector import FundamentalSector + from orix.quaternion.orientation import Orientation + from orix.vector.fundamental_sector import FundamentalSector class Symmetry(Rotation): diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index 7684e6d6..6852fd3d 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -16,80 +16,64 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . # - - -import os -from tempfile import TemporaryDirectory - from diffpy.structure import Atom, Lattice, Structure from h5py import File import matplotlib.pyplot as plt import numpy as np import pytest -from orix import constants -from orix.crystal_map import CrystalMap, PhaseList, create_coordinate_arrays -from orix.quaternion import Rotation +from orix.constants import installed +from orix.crystal_map.crystal_map import CrystalMap, create_coordinate_arrays +from orix.crystal_map.phase_list import PhaseList +from orix.quaternion.rotation import Rotation # --------------------------- pytest hooks --------------------------- # -def pytest_sessionstart(session): # pragma: no cover +def pytest_sessionstart(session): plt.rcParams["backend"] = "agg" # -------------------- Control of test selection --------------------- # skipif_numpy_quaternion_present = pytest.mark.skipif( - constants.installed["numpy-quaternion"], reason="numpy-quaternion installed" + installed["numpy-quaternion"], reason="numpy-quaternion installed" ) skipif_numpy_quaternion_missing = pytest.mark.skipif( - not constants.installed["numpy-quaternion"], reason="numpy-quaternion not installed" + not installed["numpy-quaternion"], reason="numpy-quaternion not installed" ) -# ---------------------------- IO fixtures --------------------------- # - -# ----------------------------- .ang file ---------------------------- # - def pytest_addoption(parser): parser.addoption( - "--runslow", action="store_true", default=False, help="run slow tests" + "--slow", action="store_true", default=False, help="Run slow tests" ) -def pytest_configure(config): - config.addinivalue_line("markers", "slow: mark test as slow to run") +# Markers are defined in package configuration +MARKERS = ["slow"] -def pytest_collection_modifyitems(config, items): - if config.getoption("--runslow"): - # --runslow given in cli: do not skip slow tests - return - else: # pragma: no cover - skip_slow = pytest.mark.skip(reason="need --runslow option to run") - for item in items: - if "slow" in item.keywords: - item.add_marker(skip_slow) +def pytest_runtest_setup(item): + # Skip certain tests when flag is missing: + # https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_runtest_setup + for marker in MARKERS: + marker_str = f"--{marker}" + if marker in item.keywords and not item.config.getoption(marker_str): + pytest.skip(f"Needs {marker_str} flag to run") -@pytest.fixture() -def temp_ang_file(): - with TemporaryDirectory() as tempdir: - f = open(os.path.join(tempdir, "temp_ang_file.ang"), mode="w+") - yield f +# ---------------------------- IO fixtures --------------------------- # +# ----------------------------- .ang file ---------------------------- # -@pytest.fixture(params=["h5"]) -def temp_file_path(request): - """Temporary file in a temporary directory for use when tests need - to write, and sometimes read again, data to, and from, a file. - """ - ext = request.param - with TemporaryDirectory() as tmp: - file_path = os.path.join(tmp, "data_temp." + ext) - yield file_path + +@pytest.fixture() +def temp_ang_file(tmpdir): + fname = tmpdir.join("temp_ang_file.ang") + with open(fname, mode="w+") as f: + yield f ANGFILE_TSL_HEADER = r"""# TEM_PIXperUM 1.000000 @@ -392,7 +376,7 @@ def angfile_emsoft(tmpdir, request): # Variable map shape and step sizes CTF_OXFORD_HEADER = r"""Channel Text File Prj standard steel sample -Author +Author JobMode Grid XCells %i YCells %i @@ -754,7 +738,7 @@ def ctf_emsoft(tmpdir, request): AcqE1 0.0000 AcqE2 0.0000 AcqE3 0.0000 -Euler angles refer to Sample Coordinate system (CS0)! Mag 0.0000 Coverage 0 Device 0 KV 0.0000 TiltAngle 0.0000 TiltAxis 0 DetectorOrientationE1 0.0000 DetectorOrientationE2 0.0000 DetectorOrientationE3 0.0000 WorkingDistance 0.0000 InsertionDistance 0.0000 +Euler angles refer to Sample Coordinate system (CS0)! Mag 0.0000 Coverage 0 Device 0 KV 0.0000 TiltAngle 0.0000 TiltAxis 0 DetectorOrientationE1 0.0000 DetectorOrientationE2 0.0000 DetectorOrientationE3 0.0000 WorkingDistance 0.0000 InsertionDistance 0.0000 Phases 1 4.079;4.079;4.079 90.000;90.000;90.000 Gold 11 0 Created from mtex Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" diff --git a/orix/tests/io/test_ang.py b/orix/tests/io/test_ang.py index b224bfda..f1f57e78 100644 --- a/orix/tests/io/test_ang.py +++ b/orix/tests/io/test_ang.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# import numpy as np import pytest @@ -371,23 +373,24 @@ def test_load_ang_emsoft( def test_get_header(self, temp_ang_file): temp_ang_file.write(ANGFILE_ASTAR_HEADER) temp_ang_file.close() - assert _get_header(open(temp_ang_file.name)) == [ - "# File created from ACOM RES results", - "# ni-dislocations.res", - "# ".rstrip(), - "# ".rstrip(), - "# MaterialName Nickel", - "# Formula", - "# Symmetry 43", - "# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000", - "# NumberFamilies 4", - "# hklFamilies 1 1 1 1 0.000000", - "# hklFamilies 2 0 0 1 0.000000", - "# hklFamilies 2 2 0 1 0.000000", - "# hklFamilies 3 1 1 1 0.000000", - "#", - "# GRID: SqrGrid#", - ] + with open(temp_ang_file.name) as f: + assert _get_header(f) == [ + "# File created from ACOM RES results", + "# ni-dislocations.res", + "# ".rstrip(), + "# ".rstrip(), + "# MaterialName Nickel", + "# Formula", + "# Symmetry 43", + "# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000", + "# NumberFamilies 4", + "# hklFamilies 1 1 1 1 0.000000", + "# hklFamilies 2 0 0 1 0.000000", + "# hklFamilies 2 2 0 1 0.000000", + "# hklFamilies 3 1 1 1 0.000000", + "#", + "# GRID: SqrGrid#", + ] @pytest.mark.parametrize( "expected_vendor, expected_columns, vendor_header", @@ -419,7 +422,8 @@ def test_get_vendor_columns( temp_ang_file.write(vendor_header) temp_ang_file.close() - header = _get_header(open(temp_ang_file.name)) + with open(temp_ang_file.name) as f: + header = _get_header(f) vendor, column_names = _get_vendor_columns(header, n_cols_file) assert vendor == expected_vendor @@ -429,7 +433,8 @@ def test_get_vendor_columns( def test_get_vendor_columns_unknown(self, temp_ang_file, n_cols_file): temp_ang_file.write("Look at me!\nI'm Mr. .ang file!\n") temp_ang_file.close() - header = _get_header(open(temp_ang_file.name)) + with open(temp_ang_file.name) as f: + header = _get_header(f) with pytest.warns(UserWarning, match=f"Number of columns, {n_cols_file}, "): vendor, column_names = _get_vendor_columns(header, n_cols_file) assert vendor == "unknown" diff --git a/orix/tests/io/test_h5ebsd.py b/orix/tests/io/test_h5ebsd.py index c364e71a..b309bbec 100644 --- a/orix/tests/io/test_h5ebsd.py +++ b/orix/tests/io/test_h5ebsd.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from h5py import File @@ -22,12 +24,13 @@ class TestH5ebsd: - def test_hdf5group2dict_update_dict(self, temp_file_path, crystal_map): + def test_hdf5group2dict_update_dict(self, tmp_path, crystal_map): """Can read datasets from an HDF5 file into an existing dictionary. """ - save(temp_file_path, crystal_map) - with File(temp_file_path, mode="r") as f: + fname = tmp_path / "test.h5" + save(fname, crystal_map) + with File(fname, mode="r") as f: this_dict = {"hello": "there"} this_dict = hdf5group2dict(f["crystal_map"], dictionary=this_dict) assert this_dict["hello"] == "there" diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py index fe276927..46258910 100644 --- a/orix/tests/io/test_io.py +++ b/orix/tests/io/test_io.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from collections import OrderedDict from contextlib import contextmanager @@ -73,11 +75,11 @@ def test_load_no_filename_match(self): with pytest.raises(IOError, match=f"No filename matches '{fname}'."): _ = load(fname) - @pytest.mark.parametrize("temp_file_path", ["ktf"], indirect=["temp_file_path"]) - def test_load_unsupported_format(self, temp_file_path): - np.savetxt(temp_file_path, X=np.random.rand(100, 8)) - with pytest.raises(IOError, match=f"Could not read "): - _ = load(temp_file_path) + def test_load_unsupported_format(self, tmp_path): + fname = tmp_path / "unsupported_file.ktf" + np.savetxt(fname, X=np.random.rand(100, 8)) + with pytest.raises(IOError, match="Could not read "): + _ = load(fname) @pytest.mark.parametrize( "manufacturer, expected_plugin", @@ -88,61 +90,62 @@ def test_load_unsupported_format(self, temp_file_path): ("Oxford", None), ], ) - def test_plugin_from_manufacturer( - self, temp_file_path, manufacturer, expected_plugin - ): + def test_plugin_from_manufacturer(self, manufacturer, expected_plugin, tmp_path): h5ebsd_plugin_list = [bruker_h5ebsd, emsoft_h5ebsd, orix_hdf5] - with File(temp_file_path, mode="w") as f: + fname = tmp_path / "test.h5" + with File(fname, mode="w") as f: f.create_dataset(name="Manufacturer", data=manufacturer) assert ( - _plugin_from_manufacturer(temp_file_path, plugins=h5ebsd_plugin_list) + _plugin_from_manufacturer(fname, plugins=h5ebsd_plugin_list) is expected_plugin ) - def test_overwrite_or_not(self, crystal_map, temp_file_path): - save(temp_file_path, crystal_map) + def test_overwrite_or_not(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" + save(fname, crystal_map) with pytest.warns(UserWarning, match="Not overwriting, since your terminal "): - _overwrite_or_not(temp_file_path) + _overwrite_or_not(fname) @pytest.mark.parametrize( "answer, expected", [("y", True), ("n", False), ("m", None)] ) - def test_overwrite_or_not_input( - self, crystal_map, temp_file_path, answer, expected - ): - save(temp_file_path, crystal_map) + def test_overwrite_or_not_input(self, crystal_map, answer, expected, tmp_path): + fname = tmp_path / "test.h5" + save(fname, crystal_map) if answer == "m": with replace_stdin(StringIO(answer)): with pytest.raises(EOFError): - _overwrite_or_not(temp_file_path) + _overwrite_or_not(fname) else: with replace_stdin(StringIO(answer)): - assert _overwrite_or_not(temp_file_path) is expected + assert _overwrite_or_not(fname) is expected - @pytest.mark.parametrize("temp_file_path", ["angs", "hdf4", "h6"]) - def test_save_unsupported_raises(self, temp_file_path, crystal_map): - _, ext = os.path.splitext(temp_file_path) + @pytest.mark.parametrize("ext", ["angs", "hdf4", "h6"]) + def test_save_unsupported_raises(self, ext, crystal_map, tmp_path): + fname = tmp_path / f"test.{ext}" with pytest.raises(IOError, match=f"'{ext}' does not correspond to any "): - save(temp_file_path, crystal_map) + save(fname, crystal_map) - def test_save_overwrite_raises(self, temp_file_path, crystal_map): + def test_save_overwrite_raises(self, crystal_map, tmp_path): with pytest.raises(ValueError, match="`overwrite` parameter can only be "): - save(temp_file_path, crystal_map, overwrite=1) + save(tmp_path / "test.h5", crystal_map, overwrite=1) @pytest.mark.parametrize( "overwrite, expected_phase_name", [(True, "hepp"), (False, "")] ) def test_save_overwrite( - self, temp_file_path, crystal_map, overwrite, expected_phase_name + self, crystal_map, overwrite, expected_phase_name, tmp_path ): + fname = tmp_path / "test.h5" + assert crystal_map.phases[0].name == "" - save(temp_file_path, crystal_map) - assert os.path.isfile(temp_file_path) is True + save(fname, crystal_map) + assert os.path.isfile(fname) is True crystal_map.phases[0].name = "hepp" - save(temp_file_path, crystal_map, overwrite=overwrite) + save(fname, crystal_map, overwrite=overwrite) - crystal_map2 = load(temp_file_path) + crystal_map2 = load(fname) assert crystal_map2.phases[0].name == expected_phase_name diff --git a/orix/tests/io/test_orix_hdf5.py b/orix/tests/io/test_orix_hdf5.py index 11796600..8640788c 100644 --- a/orix/tests/io/test_orix_hdf5.py +++ b/orix/tests/io/test_orix_hdf5.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from diffpy.structure.spacegroups import GetSpaceGroup from h5py import File @@ -22,7 +24,7 @@ from orix import __version__ as orix_version from orix.crystal_map import CrystalMap, Phase -from orix.io import load, save +import orix.io as oio from orix.io.plugins.orix_hdf5 import ( atom2dict, crystalmap2dict, @@ -42,10 +44,12 @@ class TestOrixHDF5Plugin: - def test_file_writer(self, crystal_map, temp_file_path): - save(filename=temp_file_path, object2write=crystal_map) + def test_file_writer(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" + + oio.save(filename=fname, object2write=crystal_map) - with File(temp_file_path) as f: + with File(fname) as f: assert f["manufacturer"][()][0].decode() == "orix" assert f["version"][()][0].decode() == orix_version @@ -57,10 +61,12 @@ def test_file_writer(self, crystal_map, temp_file_path): ], indirect=["crystal_map_input"], ) - def test_write_read_masked(self, crystal_map_input, temp_file_path): + def test_write_read_masked(self, crystal_map_input, tmp_path): + fname = tmp_path / "test.h5" + xmap = CrystalMap(**crystal_map_input) - save(filename=temp_file_path, object2write=xmap[xmap.x > 2]) - xmap2 = load(temp_file_path) + oio.save(filename=fname, object2write=xmap[xmap.x > 2]) + xmap2 = oio.load(fname) assert xmap2.size != xmap.size with pytest.raises(ValueError, match="operands could not be broadcast"): @@ -70,13 +76,14 @@ def test_write_read_masked(self, crystal_map_input, temp_file_path): assert xmap2.size == xmap.size assert np.allclose(xmap2.x, xmap.x) - def test_file_writer_raises(self, temp_file_path, crystal_map): + def test_file_writer_raises(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" with pytest.raises(OSError, match="Cannot write to the already open file "): - with File(temp_file_path, mode="w") as _: - save(temp_file_path, crystal_map, overwrite=True) + with File(fname, mode="w") as _: + oio.save(fname, crystal_map, overwrite=True) - def test_dict2hdf5group(self, temp_file_path): - with File(temp_file_path, mode="w") as f: + def test_dict2hdf5group(self, tmp_path): + with File(tmp_path / "test.h5", mode="w") as f: group = f.create_group(name="a_group") with pytest.warns(UserWarning, match="The orix HDF5 writer could not"): dict2hdf5group( @@ -141,9 +148,11 @@ def test_structure2dict(self, phase_list): assert np.allclose(lattice1["baserot"], lattice2["baserot"]) assert_dictionaries_are_equal(structure_dict["atoms"], this_dict["atoms"]) - def test_file_reader(self, crystal_map, temp_file_path): - save(filename=temp_file_path, object2write=crystal_map) - xmap2 = load(filename=temp_file_path) + def test_file_reader(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" + + oio.save(filename=fname, object2write=crystal_map) + xmap2 = oio.load(filename=fname) assert_dictionaries_are_equal(crystal_map.__dict__, xmap2.__dict__) def test_dict2crystalmap(self, crystal_map): @@ -207,7 +216,7 @@ def test_dict2atom(self, phase_list): assert str(atom.element) == str(atom2.element) assert np.allclose(atom.xyz, atom2.xyz) - def test_write_read_nd_crystalmap_properties(self, temp_file_path, crystal_map): + def test_write_read_nd_crystalmap_properties(self, crystal_map, tmp_path): """Crystal map properties with more than one value in each point (e.g. top matching scores from dictionary indexing) can be written and read from file correctly. @@ -225,8 +234,9 @@ def test_write_read_nd_crystalmap_properties(self, temp_file_path, crystal_map): prop3d = np.arange(map_size * 4).reshape(prop3d_shape) xmap.prop[prop3d_name] = prop3d - save(filename=temp_file_path, object2write=xmap) - xmap2 = load(temp_file_path) + fname = tmp_path / "test.h5" + oio.save(filename=fname, object2write=xmap) + xmap2 = oio.load(fname) assert np.allclose(xmap2.prop[prop2d_name], xmap.prop[prop2d_name]) assert np.allclose(xmap2.prop[prop3d_name], xmap.prop[prop3d_name]) diff --git a/orix/vector/miller.py b/orix/vector/miller.py index 897898ce..2511a95a 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # orix is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with orix. If not, see . +# along with orix. If not, see . +# from __future__ import annotations @@ -21,12 +23,6 @@ from itertools import product from typing import TYPE_CHECKING, Optional, Tuple, Union -try: - # New in Python 3.11 - from typing import Self -except ImportError: # pragma: no cover - from typing_extensions import Self - from diffpy.structure import Lattice import numpy as np @@ -344,7 +340,7 @@ def is_hexagonal(self) -> bool: return self.phase.is_hexagonal @property - def unit(self) -> Self: + def unit(self) -> Miller: """Return unit vectors.""" m = self.__class__(xyz=super().unit.data, phase=self.phase) m.coordinate_format = self.coordinate_format @@ -363,7 +359,7 @@ def __repr__(self) -> str: f"{name} {shape}, point group {symmetry}, {coordinate_format}\n" f"{data}" ) - def __getitem__(self, key) -> Self: + def __getitem__(self, key) -> Miller: """NumPy fancy indexing of vectors.""" m = self.__class__(xyz=self.data[key], phase=self.phase).deepcopy() m.coordinate_format = self.coordinate_format @@ -378,7 +374,7 @@ def from_highest_indices( uvw: Union[np.ndarray, list, tuple, None] = None, hkl: Union[np.ndarray, list, tuple, None] = None, include_zero_vector: bool = False, - ) -> Self: + ) -> Miller: """Create a set of unique direct or reciprocal lattice vectors from three highest indices and a phase (crystal lattice and symmetry). @@ -405,7 +401,7 @@ def from_highest_indices( return cls(**init_kw).unique() @classmethod - def from_min_dspacing(cls, phase: "Phase", min_dspacing: float = 0.05) -> Self: + def from_min_dspacing(cls, phase: "Phase", min_dspacing: float = 0.05) -> Miller: """Create a set of unique reciprocal lattice vectors with a a direct space interplanar spacing greater than a lower threshold. @@ -432,7 +428,7 @@ def random( phase: "Phase", shape: Union[int, tuple] = 1, coordinate_format: str = "xyz", - ) -> Self: + ) -> Miller: """Create random Miller indices. Parameters @@ -466,11 +462,11 @@ def random( # --------------------- Other public methods --------------------- # - def deepcopy(self) -> Self: + def deepcopy(self) -> Miller: """Return a deepcopy of the instance.""" return deepcopy(self) - def round(self, max_index: int = 20) -> Self: + def round(self, max_index: int = 20) -> Miller: """Round a set of index triplet (Miller) or quartet (Miller-Bravais/Weber) to the *closest* smallest integers. @@ -498,7 +494,9 @@ def symmetrise( unique: bool = False, return_multiplicity: bool = False, return_index: bool = False, - ) -> Union[Self, Tuple[Self, np.ndarray], Tuple[Self, np.ndarray, np.ndarray]]: + ) -> Union[ + Miller, Tuple[Miller, np.ndarray], Tuple[Miller, np.ndarray, np.ndarray] + ]: """Return vectors symmetrically equivalent to the vectors. Parameters @@ -585,7 +583,7 @@ def symmetrise( def angle_with( self, - other: Self, + other: Miller, use_symmetry: bool = False, degrees: bool = False, ) -> np.ndarray: @@ -630,7 +628,7 @@ def angle_with( return angles - def cross(self, other: Self) -> Self: + def cross(self, other: Miller) -> Miller: """Return the cross products of the vectors with the other vectors, which is considered the zone axes between the vectors. @@ -652,7 +650,7 @@ def cross(self, other: Self) -> Self: m.coordinate_format = new_fmt[self.coordinate_format] return m - def dot(self, other: Self) -> np.ndarray: + def dot(self, other: Miller) -> np.ndarray: """Return the dot products of the vectors and the other vectors. Parameters @@ -669,7 +667,7 @@ def dot(self, other: Self) -> np.ndarray: self._compatible_with(other, raise_error=True) return super().dot(other) - def dot_outer(self, other: Self) -> np.ndarray: + def dot_outer(self, other: Miller) -> np.ndarray: """Return the outer dot products of the vectors and the other vectors. @@ -687,7 +685,7 @@ def dot_outer(self, other: Self) -> np.ndarray: self._compatible_with(other, raise_error=True) return super().dot_outer(other) - def flatten(self) -> Self: + def flatten(self) -> Miller: """Return the flattened vectors. Returns @@ -699,7 +697,7 @@ def flatten(self) -> Self: m.coordinate_format = self.coordinate_format return m - def transpose(self, *axes: Optional[int]) -> Self: + def transpose(self, *axes: Optional[int]) -> Miller: """Return a new instance with the data transposed. The order may be undefined if :attr:`ndim` is originally 2. In @@ -725,7 +723,7 @@ def get_nearest(self, *args) -> NotImplemented: """NotImplemented.""" return NotImplemented - def mean(self, use_symmetry: bool = False) -> Self: + def mean(self, use_symmetry: bool = False) -> Miller: """Return the mean vector of the set of vectors. Parameters @@ -745,7 +743,7 @@ def mean(self, use_symmetry: bool = False) -> Self: m.coordinate_format = self.coordinate_format return m - def reshape(self, *shape: Union[int, tuple]) -> Self: + def reshape(self, *shape: Union[int, tuple]) -> Miller: """Return a new instance with the vectors reshaped. Parameters @@ -764,7 +762,7 @@ def reshape(self, *shape: Union[int, tuple]) -> Self: def unique( self, use_symmetry: bool = False, return_index: bool = False - ) -> Union[Self, Tuple[Self, np.ndarray]]: + ) -> Union[Miller, Tuple[Miller, np.ndarray]]: """Unique vectors in ``self``. Parameters @@ -809,7 +807,7 @@ def unique( else: return m - def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Self: + def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Miller: """Project Miller indices to a symmetry's fundamental sector (inverse pole figure). @@ -856,7 +854,7 @@ def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Self: # -------------------- Other private methods --------------------- # - def _compatible_with(self, other: Self, raise_error: bool = False) -> bool: + def _compatible_with(self, other: Miller, raise_error: bool = False) -> bool: """Whether ``self`` and ``other`` are the same (the same crystal lattice and symmetry) with vectors in the same space. diff --git a/pyproject.toml b/pyproject.toml index ec28e9d9..314d5025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,6 @@ dependencies = [ "pycifrw", "scipy", "tqdm", - # TODO: Remove once Python >= 3.11 - "typing_extensions", ] [project.optional-dependencies] @@ -49,7 +47,8 @@ doc = [ "memory_profiler", "nbconvert >= 7.16.4", "nbsphinx >= 0.7", - "numpydoc", + # Restriction due to https://github.com/pyxem/orix/issues/570 + "numpydoc != 1.9.0", "pydata-sphinx-theme", "scikit-image", "scikit-learn", @@ -100,12 +99,16 @@ addopts = [ "-ra", "--ignore=doc/_static/img/colormap_banners/create_colormap_banners.py", "--ignore=examples/*/*.py", + "--strict-markers", ] doctest_optionflags = "NORMALIZE_WHITESPACE" filterwarnings = [ "ignore:Deprecated call to `pkg_resources:DeprecationWarning", "ignore:pkg_resources is deprecated as an API:DeprecationWarning", ] +markers = [ + "slow: mark test as slow", +] [tool.isort] profile = "black"