diff --git a/docs/source/api.rst b/docs/source/api.rst index 72bd32d..8cbf0f8 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -57,7 +57,9 @@ correctly handles them. .. _nographs.Weight: .. code-block:: python - class Weight(Protocol[T]): + T = TypeVar("T") + + class Weight(Protocol): @abstractmethod def __add__(self: T, value: T) -> T: ... # self + value @abstractmethod diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ed0902e..bd98ea6 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,6 +1,20 @@ ChangeLog --------- +**v3.4.1** (2024-12-26) + + - Python 3.13 officially supported. + + - The source code of this version is adapted so that it can be compiled + with MyPyC and with Cython if required. See also the notes in + requirements.dev, pyproject.toml, and setup.py. + This is not guaranteed for future versions. + (In the case of NoGraphs, however, compiling does not result in a + significant increase in speed: MyPyC can currently only store C-native + values boxed in collections, which requires frequent conversion. + And Cython only generates efficient C code here if the Python code is + specially adapted). + **v3.4.0** (2024-07-25) - Method TraversalDepthsFirst.start_from: New parameters: diff --git a/docs/source/gadgets.rst b/docs/source/gadgets.rst index a8eaab7..52b626c 100644 --- a/docs/source/gadgets.rst +++ b/docs/source/gadgets.rst @@ -367,12 +367,19 @@ and calculate the manhattan distance of another vector to our position**: >>> # Position vector multiplied by an integer value, returns Position >>> nog.Position.at(2, 3, 4) * 3 (6, 9, 12) - >>> # Attention: Since a Position is a tuple, i * Position repeats the coordinates + >>> # Attention: For i * Position, Position behaves like a tuple + >>> # and repeats the coordinates >>> 3 * nog.Position.at(2,3,4) (2, 3, 4, 2, 3, 4, 2, 3, 4) >>> # Manhattan distance of some vector >>> nog.Position.at(2, 3, 4).manhattan_distance( (1, 1, 1) ) 6 + >>> # Compares equal to a tuple that contains the same coordinates + >>> nog.Position.at(2, 3, 4) == (2, 3, 4) + True + >>> # Hashable, and can be used as element in sets + >>> len(set(2 * [nog.Position.at(1,1)])) + 1 When we use vector addition or subtraction to "move" some increment away from a position, we could "leave" some coordinate ranges we would like to stay in. diff --git a/docs/source/gears.rst b/docs/source/gears.rst index 56b976e..245d521 100644 --- a/docs/source/gears.rst +++ b/docs/source/gears.rst @@ -253,6 +253,11 @@ Side note about the implementation: Defining your own gear ~~~~~~~~~~~~~~~~~~~~~~ +.. note:: + + The functionality described in this section cannot be used when NoGraphs + is compiled by MyPyC. + You can define your own gear by subclassing one of the gear classes described in the previous section and overwriting one of more of the factory methods. @@ -291,11 +296,21 @@ id set for given vertices, by an implementation that returns an *intbitset*. .. code-block:: python - >>> class GearBitsetAndArrayForIntVerticesAndCFloats( - ... nog.GearForIntVerticesAndIDsAndCFloats - ... ): - ... def vertex_id_set(self, vertices): - ... return intbitset(list(vertices)) + >>> def is_mypyc_compiled() -> bool: + ... """ + ... Recognize by the file extension whether MyPyC-compiled code is running. + ... """ + ... return not __file__.endswith(".py") + >>> if is_mypyc_compiled(): + ... # If NoGraphs is compiled by MyPyC, skip this example + ... GearBitsetAndArrayForIntVerticesAndCFloats = \ + ... nog.GearForIntVerticesAndIDsAndCFloats + ... else: + ... class GearBitsetAndArrayForIntVerticesAndCFloats( + ... nog.GearForIntVerticesAndIDsAndCFloats + ... ): + ... def vertex_id_set(self, vertices): + ... return intbitset(list(vertices)) We can use the new gear just like the predefined ones: @@ -304,9 +319,9 @@ We can use the new gear just like the predefined ones: >>> our_gear = GearBitsetAndArrayForIntVerticesAndCFloats() >>> traversal = nog.TraversalBreadthFirstFlex( ... next_edges=next_edges, gear=our_gear, vertex_to_id=nog.vertex_as_id) - >>> traversal.start_from(0).go_to(1200000) #doctest:+SLOW_TEST + >>> print(traversal.start_from(0).go_to(1200000)) #doctest:+SLOW_TEST 1200000 - >>> traversal.depth #doctest:+SLOW_TEST + >>> print(traversal.depth) #doctest:+SLOW_TEST 200000 Section `Comparison of NoGraphs gears ` shows the diff --git a/docs/source/traversals.rst b/docs/source/traversals.rst index d169594..eaf3c9f 100644 --- a/docs/source/traversals.rst +++ b/docs/source/traversals.rst @@ -659,8 +659,10 @@ If such an event occurs, **no vertex has been entered, and** it is therefor **not allowed to signal to the traversal to skip the entered (!) vertex**. If you do this anyway, the traversal intentionally won’t catch the -*StopIteration* you throw, and a *RuntimeError* will be raised -(according to `PEP 497 `_). +*StopIteration* you throw, and an exception will be raised +(CPython and PyPy will raise a *RuntimeError* according to +`PEP 497 `_, and with NoGraphs as extension module +compiled by MyPyC, the StopException will fall through.). This also means, that it is always save to ignore the return value of throwing the *StopIteration* into the generator: it can only be the entered @@ -680,7 +682,7 @@ by the generator and the generator skips expanding the vertex. Then, vertex *A* is reported with event *SKIPPING_START*. This means, *A* it is not entered. Here, throwing *StopIteration* is not -accepted and a *RuntimeError* is raised. +accepted and an exception is raised. .. code-block:: python @@ -694,9 +696,11 @@ accepted and a *RuntimeError* is raised. 'A' >>> next(generator), str(traversal.event) ('A', 'DFSEvent.SKIPPING_START') - >>> generator.throw(StopIteration()) - Traceback (most recent call last): - RuntimeError: generator raised StopIteration + >>> try: + ... generator.throw(StopIteration()) + ... except (RuntimeError, StopIteration): + ... print("Exception caught") + Exception caught .. _is_tree: diff --git a/pyproject.toml b/pyproject.toml index 6a00d58..e15123e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nographs" -version = "3.4.0" +version = "3.4.1" authors = [ { name="Dr. Helmut Melcher", email="HeWeMel@web.de" }, ] @@ -23,6 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Intended Audience :: Developers", @@ -44,6 +45,24 @@ Changelog = "https://nographs.readthedocs.io/en/latest/changelog.html" [tool.coverage.report] exclude_lines =["pragma: no cover", "@overload"] +[tool.mypy] +# Specified the target platform details in config, so the developers are +# free to run mypy on Windows, Linux, or macOS and get consistent +# results. +python_version = "3.9" +mypy_path = "src/nographs tests" +strict = true +disallow_untyped_defs = true +warn_unreachable = true +implicit_reexport = true +show_error_codes = true +show_column_numbers = true + [build-system] -requires = ["setuptools>=61.0"] +requires = [ + "setuptools>=61.0", + "wheel ~=0.37.1", + # "cython", # Only needed if Cython-compiled binary wheel is demanded + # "mypy[mypyc]", # Only needed if MyPyC-compiled binary wheel is demanded +] build-backend = "setuptools.build_meta" diff --git a/requirements_dev.txt b/requirements_dev.txt index 5405348..7ad8b7d 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -12,8 +12,14 @@ coverage>=6.3 flake8 flake8-bugbear mypy +# -- For building a package with C-extensions compiled by MyPyC (optional)-- +# mypy[mypyc] +# mypy-extensions +# -- For building a package with C-extensions compiled by Cython (optional) -- +# cython # -- source consistency checking -- pymacros4py>=0.8.2 ; python_version >= "3.10" # -- additional packages for tests using them -- mpmath -intbitset ; python_version < "3.13" and implementation_name == "cpython" # missing on PyPy and often on new CPython-versions +# The following is missing on PyPy and often on new CPython-versions +intbitset ; python_version < "3.13" and implementation_name == "cpython" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..707b96b --- /dev/null +++ b/setup.py @@ -0,0 +1,80 @@ +# This file controls the compilation of the modules of the package to extension +# modules by using MyPyC or Cython. If this option is not needed, it can be removed. +# +# Usage: If environment variable SETUP_BUILD_EXTENSION is set to MyPyC or Cython, +# extension modules are build. And if it is set to "False", or not set at all, +# the pure Python modules are copied into the wheel. + +import pathlib +import os +from setuptools import setup, find_packages + + +def find_sources(exclude_from_compilation): + compile_this = [] + for file_path in pathlib.Path('.', 'src', 'nographs').glob('**/*.py'): + if file_path.name in exclude_from_compilation: + print('setup.py: We will skip the compilation of file', file_path) + continue + compile_this.append(str(file_path)) + print('setup.py: We will compile these files:\n', compile_this, "\n") + return compile_this + + +compiler = os.environ.get('SETUP_BUILD_EXTENSION', "False").capitalize() +match compiler: + case "False": + print("\nsetup.py: Building sdist (tarball) / and or pure Python wheel.") + print("(Set environment variable SETUP_BUILD_EXTENSION to MyPyC or Cython " + "to compile binaries.)") + ext_modules = [] + + case "Mypyc": + print(f"\nsetup.py: Compiling binaries using MyPyC.") + print("(Set environment variable SETUP_BUILD_EXTENSION to False " + "to build sdist / and or pure Python wheel instead.)") + + from mypyc.build import mypycify + exclude_from_compilation = [ + # The following file contains classes, compilation would be useful, but + # it is intentionally not compiled here due to the following issue of + # MyPyC: + # https://github.com/mypyc/mypyc/issues/1022 + 'depth_first_enum_types.py', + # The following file subclasses tuple[int]. MyPyC does not support this. + # But on CPython this us much faster than to store the tuple in an attribute + # Conditional class definition is also not supported. So, we simply exclude + # this file from compilation. + '_extra_matrix_gadgets.py', + ] + compile_this = find_sources(exclude_from_compilation) + ext_modules = mypycify(compile_this, strip_asserts=False) + + case "Cython": + print(f"\nsetup.py: Compiling binaries using Cython.") + print("(Set environment variable SETUP_BUILD_EXTENSION to False " + "to build sdist / and or pure Python wheel instead.)") + from Cython.Build import cythonize + exclude_from_compilation = [] + compile_this = find_sources(exclude_from_compilation) + ext_modules = cythonize(compile_this, compiler_directives={'language_level': 3}) + + case _: + raise RuntimeError( + "Valid values or environment variable SETUP_BUILD_EXTENSION are:" + " 'False', 'MyPyC', and 'Cython'" + "If no value is set, this equals to 'False'.") + +if ext_modules: + setup( + name='nographs', + package_dir={'': 'src'}, + packages=find_packages('src'), + ext_modules=ext_modules, + ) +else: + setup( + name='nographs', + package_dir={'': 'src'}, + packages=find_packages('src'), + ) diff --git a/src/nographs/_compatibility.py b/src/nographs/_compatibility.py index 9c32ed9..67bc53f 100644 --- a/src/nographs/_compatibility.py +++ b/src/nographs/_compatibility.py @@ -7,12 +7,12 @@ # --- Solve 3.9 compatibility issue -def _pairwise(iterable: Iterable[T]) -> Iterator[tuple[T, T]]: +def _manual_pairwise(iterable: Iterable[T]) -> Iterator[tuple[T, T]]: """Returns an iterator of paired items, overlapping, from the original. On Python 3.10 and above, this is replaced by an alias for :func:`itertools.pairwise`. - >>> list(_pairwise("abc")) + >>> list(_manual_pairwise("abc")) [('a', 'b'), ('b', 'c')] """ a, b = itertools.tee(iterable) @@ -20,13 +20,35 @@ def _pairwise(iterable: Iterable[T]) -> Iterator[tuple[T, T]]: yield from zip(a, b) +# Under Python 3.9, detect that itertools.pairwise is missing, and replace it by a +# manual implementation. Under Python >3.9, use build-in function. try: - from itertools import pairwise as itertools_pairwise # type: ignore[attr-defined] + # Under 3.9, MyPy need to ignore that pairwise is missing. Under >3.9, it needs + # to ignore, that the ignore statement is not needed. + from itertools import ( # type: ignore[attr-defined,unused-ignore] + pairwise as itertools_pairwise, + ) except ImportError: # pragma: no cover # not executed under Python >=3.10 - pairwise = _pairwise + pairwise = _manual_pairwise else: # pragma: no cover # not executed under Python <3.10 + # We cannot assign itertools_pairwise (type: type[pairwise[Any]] to + # pairwise (type "Callable[[Iterable[T]], Iterator[tuple[T, T]]]", see above). + # So, we need to also manually implement a wrapper around itertools_pairwise. def pairwise(iterable: Iterable[T]) -> Iterator[tuple[T, T]]: yield from itertools_pairwise(iterable) - pairwise.__doc__ = _pairwise.__doc__ + # The following would be nice, but with PyPyC, __doc__ is not writable. + # pairwise.__doc__ = _pairwise.__doc__ + + +# --- MyPyC issues --- + +try: + from mypy_extensions import trait +except ( + ModuleNotFoundError +): # pragma: no cover # Not reachable if mypy_extensions are installed + + def trait(cls: T) -> T: + return cls diff --git a/src/nographs/_extra_edge_gadgets.py b/src/nographs/_extra_edge_gadgets.py index eb29976..a99c27e 100644 --- a/src/nographs/_extra_edge_gadgets.py +++ b/src/nographs/_extra_edge_gadgets.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import collections import itertools from collections.abc import Sequence, Mapping, Callable, Iterable @@ -55,8 +53,11 @@ def adapt_edge_index( def adapt_edge_index( - index: Union[Mapping, Sequence], *, add_inverted: bool, attributes: bool -) -> Callable: + index: Union[Mapping[Any, Any], Sequence[Any]], + *, + add_inverted: bool, + attributes: bool, +) -> Callable[[Any, Any], Any]: """ Read a graph from a Mapping (e.g. a Dict) or from a Sequence (e.g. a tuple or list, if integers are used as the vertices) and provide a neighbor function @@ -164,8 +165,8 @@ def adapt_edge_iterable( def adapt_edge_iterable( - edges: Iterable[Sequence], *, add_inverted: bool, attributes: bool -) -> Callable: + edges: Iterable[Sequence[Any]], *, add_inverted: bool, attributes: bool +) -> Callable[[Any, Any], Any]: """ Read a graph from an Iterable of edges and provide a neighbor function (`NextVertices` or `NextEdges`) from that data. Typically only used for test @@ -195,7 +196,9 @@ def adapt_edge_iterable( :return: Neighbor function that can be used as parameter for one of the traversal algorithms. See `OutEdge ` for the case of attributes. """ - edge_dict: dict[Any, list[Any]] = collections.defaultdict(list) + # Cython: edge_dict was declared as dict[Any, list[Any]], but this fails + # on Cython, because Cython does not accept a defaultdict as dict. + edge_dict = collections.defaultdict[Any, list[Any]](list) if add_inverted: if attributes: # Labeled edges are provided and all data should be used diff --git a/src/nographs/_extra_matrix_gadgets.py b/src/nographs/_extra_matrix_gadgets.py index 9865807..4baccfa 100644 --- a/src/nographs/_extra_matrix_gadgets.py +++ b/src/nographs/_extra_matrix_gadgets.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import itertools import operator from collections.abc import ( @@ -22,31 +20,35 @@ Limits = Sequence[tuple[int, int]] # api.rst: documented manually +# MyPyC: tuple[int] cannot be subclassed. But on CPython, storing the tuple +# in an attribute makes indexed access much slower. Thus, this class is not +# compiled with MyPyC. class Position(tuple[int]): """A position in an n-dimensional array. It is initialized - by a `Vector`.""" + by a `Vector`. + + It is quite slow, due to its n-dimensional character. + """ @classmethod - def at(cls, *coordinates: int) -> Position: + def at(cls, *coordinates: int) -> "Position": """Factory method, that creates a position from coordinates given as separated parameters. """ return Position(coordinates) - def __add__(self, other: Vector) -> Position: # type: ignore[override] + def __add__(self, other: Vector) -> "Position": # type: ignore[override] """Add the *other* `Vector` to the position""" return Position(map(sum, zip(self, other))) - def __sub__(self, other: Vector) -> Position: + def __sub__(self, other: Vector) -> "Position": """Subtract the *other* `Vector` from the position.""" return Position(map(operator.sub, self, other)) - def __mul__(self, multiple: SupportsIndex) -> Position: + def __mul__(self, multiple: SupportsIndex) -> "Position": """Multiply each coordinate by the multiple, typically an integer. - Attention: Since Position is a tuple, 3*p returns a tuple that - repeats the coordinates of p three times, whilst p*3 means - p.__mul__(3) and really multiplies each coordinate of p by 3. + Attention: 3*p means something else, see *__rmul__*. """ i = int(multiple) return Position(coordinate * i for coordinate in self) @@ -65,7 +67,7 @@ def is_in_cuboid(self, limits: Limits) -> bool: for coordinate, (low_limit, high_limit) in zip(self, limits) ) - def wrap_to_cuboid(self, limits: Limits) -> Position: + def wrap_to_cuboid(self, limits: Limits) -> "Position": """If a coordinate of the position is outside its respective limit range *(from, to)* of the *limits* sequence (see `Limits`), add or subtract the size (to - from + 1) of the limit range as often @@ -126,7 +128,7 @@ def neighbors( moves: Iterable[Vector], limits: Optional[Limits] = None, wrap: bool = False, - ) -> Iterator[Position]: + ) -> Iterator["Position"]: # noinspection PyShadowingNames """ Iterate the positions that are reached by performing the given moves. @@ -154,7 +156,7 @@ def neighbors( class Array: - def __init__(self, nested_sequences: Sequence, dimensions: int = 2) -> None: + def __init__(self, nested_sequences: Sequence[Any], dimensions: int = 2) -> None: """An n-dimensional array. Based on *nested sequences* that, up to a given number of @@ -189,10 +191,10 @@ def limits(self) -> Limits: in this dimension.""" return [(0, upper) for upper in self.size()] - def mutable_copy(self) -> Array: + def mutable_copy(self) -> "Array": """Create a mutable copy of the array.""" - def _writable(area: Sequence, dimensions: int) -> list: + def _writable(area: Sequence[Any], dimensions: int) -> list[Any]: if dimensions > 1: return [_writable(sub_area, dimensions - 1) for sub_area in area] else: @@ -225,7 +227,7 @@ def __setitem__(self, position: Vector, content: Any) -> None: field = self.content for coordinate in position[:-1]: field = field[coordinate] - cast(MutableSequence, field)[position[-1]] = content + cast(MutableSequence[Any], field)[position[-1]] = content def items(self) -> Iterator[tuple[Position, Any]]: """Iterate positions and content. @@ -237,7 +239,7 @@ def items(self) -> Iterator[tuple[Position, Any]]: # evaluate the type parameters of tuple (whilst typing.Tuple works). def _items_in_dimension( - area: Sequence, dimensions: int + area: Sequence[Any], dimensions: int ) -> Iterator[tuple[tuple[int, ...], Any]]: if dimensions == 1: for coordinate, sub_area in enumerate(area): @@ -267,7 +269,7 @@ def findall(self, content: Iterable[Any]) -> tuple[Position, ...]: content_set = set(content) def find_in_dimension( - p_matrix: Sequence, p_dimensions: int + p_matrix: Sequence[Any], p_dimensions: int ) -> Iterator[tuple[int, ...]]: if p_dimensions == 1: for coordinate, cell_content in enumerate(p_matrix): @@ -287,7 +289,7 @@ def next_vertices_from_forbidden( forbidden: Iterable[Hashable], wrap: bool = False, diagonals: bool = False, - ) -> Callable: + ) -> Callable[[Any, Any], Any]: # noinspection PyShadowingNames """Return a `NextVertices` function for traversal strategies, based on given choice of when positions qualify as neighbors (goals of a diff --git a/src/nographs/_extra_tsp.py b/src/nographs/_extra_tsp.py index e33f324..75c53bc 100644 --- a/src/nographs/_extra_tsp.py +++ b/src/nographs/_extra_tsp.py @@ -37,7 +37,7 @@ def __getitem__(self, item: T_key_contra) -> Optional[T_value_co]: If the Gettable does not contain *item*, a KeyError or an IndexError might be raised, or None is returned. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # -- Support functions --- diff --git a/src/nographs/_gear_collections.py b/src/nographs/_gear_collections.py index 98bdc4f..e7b4624 100644 --- a/src/nographs/_gear_collections.py +++ b/src/nographs/_gear_collections.py @@ -9,10 +9,13 @@ import sys from collections.abc import Iterable, Hashable, Iterator, MutableSet, MutableMapping -from typing import Protocol, TypeVar, Generic, Callable, Union, Optional, cast +from typing import Protocol, TypeVar, Callable, Union, Optional, cast from itertools import repeat from abc import ABC, abstractmethod +# MyPyC +from ._compatibility import trait + T_hashable_key = TypeVar("T_hashable_key", bound=Hashable) T_hashable_key_contra = TypeVar( @@ -123,13 +126,14 @@ def __getitem__(self, item: T_hashable_key_contra) -> T_value_co: """Get the value that is stored for key *item*. If *item* exceeds the internal size of the container, an IndexError is raised. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def __setitem__(self, item: T_hashable_key_contra, value: T_value_contra) -> None: """Store *value* for key *item* in the container. If *item* exceeds the internal size of the container, an IndexError is raised. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable class SequenceForGearProto( @@ -154,28 +158,33 @@ class SequenceForGearProto( with suitable type restrictions. """ + @abstractmethod def __len__(self) -> int: """Return the size of the collection.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def append(self, value: T_value_contra) -> None: """Store *value* at the end of the collection. The size of the collection raises by 1.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def extend(self, values: Iterable[T_value_contra]) -> None: """Store *values* at the end of the collection. The size of the collection raises accordingly.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def __iter__(self) -> Iterator[T_value_co]: """Return an iterator that iterates the contained values.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable # --- Protocols for the used kind of wrappers +@trait class VertexSequenceWrapperProto( Protocol[T_hashable_key_contra, T_value_contra, T_value_co] ): @@ -208,6 +217,7 @@ class VertexSequenceWrapperProto( VertexSequenceWrapperProto and the concrete implementation had to be separated.) """ + @abstractmethod def sequence( self, ) -> GettableSettableForGearProto[ @@ -216,15 +226,17 @@ def sequence( """Return the wrapped sequence. Allows NoGraphs' traversal algorithms to directly read from and write to the sequence. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def default(self) -> T_value_co: """Return the default value. When NoGraphs' traversal algorithms retrieve this value for some key, they need to interpret this as if no value is assigned to the key. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def extend_and_set(self, key: T_hashable_key_contra, value: T_value_contra) -> None: """Extend the underlying sequence in order to store *value* for *key*. For new keys "below" *key* and, for better overall performance, also @@ -237,32 +249,36 @@ def extend_and_set(self, key: T_hashable_key_contra, value: T_value_contra) -> N the call to the method is not as performance critical as the direct accesses to the underlying sequence. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable +@trait class VertexSequenceWrapperForSetProto( VertexSequenceWrapperProto[T_hashable_key, T_value_contra, T_value_co], Protocol ): """VertexSequenceWrapperProto for a wrapper that emulates a VertexSet""" + @abstractmethod def update_from_keys(self, elements: Iterable[T_hashable_key]) -> None: """Store the elements.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def index_and_bit_method( self, ) -> Optional[Callable[[T_hashable_key, int], tuple[T_hashable_key, int]]]: """Return a function that computes the integer index and the bit number for the given *key* and the given number of bits that each integer can handle. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def _from_iterable( self, elements: Iterable[T_hashable_key] - ) -> "VertexSequenceWrapperForSetProto": + ) -> "VertexSequenceWrapperForSetProto[T_hashable_key, T_value_contra, T_value_co]": """Return a new VertexSequenceWrapperForSetProto with the *elements* as content.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable class VertexSequenceWrapperForMappingProto( @@ -271,14 +287,16 @@ class VertexSequenceWrapperForMappingProto( ): """VertexSequenceWrapperProto for a wrapper that emulates a VertexMapping""" + @abstractmethod def update_from_keys_values( self, elements: Iterable[tuple[T_hashable_key_contra, T_value_contra]] ) -> None: """For each pair of key and value given by *elements*, assign the value to the key. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable + @abstractmethod def update_default( self, elements: Iterable[tuple[T_hashable_key_contra, T_value_contra]] ) -> None: @@ -286,7 +304,7 @@ def update_default( the value to the key if the collection does not already store a value (other than the default value) for the key. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable # --- ABCs and helper functions to allow for type-save down casts @@ -367,17 +385,23 @@ def update_default( def _from_iterable( self, elements: Iterable[T_hashable_key] - ) -> "_VertexSequenceWrapperAssertNoCall": + ) -> ( + "_VertexSequenceWrapperAssertNoCall[T_hashable_key, T_value_contra, T_value_co]" + ): raise AssertionError(called_by_mistake) # - Set +@trait class _VertexSetByWrapper( VertexSequenceWrapperForSetProto[T_hashable_key, int, int], VertexSet[T_hashable_key], - ABC, + # MyPyC: Prevent the following error: + # TypeError: Cannot create a consistent method resolution + # order (MRO) for bases ABC, Generic + # ABC, ): """ A VertexSet that is implemented based on a sequence-like internal container @@ -454,6 +478,7 @@ def do_not_call( # - Mapping (without None) +@trait class _VertexMappingByWrapper( VertexSequenceWrapperForMappingProto[T_hashable_key, T_value, T_value], VertexMapping[T_hashable_key, T_value], @@ -470,7 +495,7 @@ class _VertexMappingByWrapper( def get_wrapper_from_vertex_mapping( - vertex_mapping: VertexMapping[T_hashable_key, T_value] + vertex_mapping: VertexMapping[T_hashable_key, T_value], ) -> Optional[VertexSequenceWrapperForMappingProto[T_hashable_key, T_value, T_value]]: """If *vertex_mapping* is implemented by a _VertexMappingByWrapper, return its wrapper, otherwise return None. The class encapsulates the @@ -489,7 +514,7 @@ def get_wrapper_from_vertex_mapping( def access_to_vertex_mapping( - vertex_mapping: VertexMapping[T_hashable_key, T_value] + vertex_mapping: VertexMapping[T_hashable_key, T_value], ) -> tuple[ bool, GettableSettableForGearProto[T_hashable_key, T_value, T_value], @@ -531,6 +556,7 @@ def access_to_vertex_mapping( # - Mapping with None +@trait class _VertexMappingByWrapperWithNone( VertexSequenceWrapperForMappingProto[T_hashable_key, T_value, Optional[T_value]], VertexMapping[T_hashable_key, T_value], @@ -547,7 +573,7 @@ class _VertexMappingByWrapperWithNone( def get_wrapper_from_vertex_mapping_with_none( - vertex_mapping: VertexMapping[T_hashable_key, T_value] + vertex_mapping: VertexMapping[T_hashable_key, T_value], ) -> Optional[ VertexSequenceWrapperForMappingProto[T_hashable_key, T_value, Optional[T_value]] ]: @@ -570,7 +596,7 @@ def get_wrapper_from_vertex_mapping_with_none( def access_to_vertex_mapping_expect_none( - vertex_mapping: VertexMapping[T_hashable_key, T_value] + vertex_mapping: VertexMapping[T_hashable_key, T_value], ) -> tuple[ bool, GettableSettableForGearProto[T_hashable_key, T_value, Optional[T_value]], @@ -616,7 +642,7 @@ def access_to_vertex_mapping_expect_none( class VertexSequenceWrapper( - Generic[T_value, T_default_value], + # Generic[T_value, T_default_value], VertexSequenceWrapperProto[ NonNegativeDenseInt, T_value, Union[T_value, T_default_value] ], @@ -639,7 +665,23 @@ class VertexSequenceWrapper( :param default: The default value. - :param extend_size: Number of pre-allocated item slots when the collection grows + :param extend_size: Minimal number of item slots added each time when the + collection grows. A size other than 1 can be used to ensure that + the collection does not need to grow too often, since growing is + expensive (the strategies of NoGraphs cannot do it on their own but + need to call extend_and_set() - and this should not happen too often. + + Note: The collection itself needs to optimize the growing + process by growing more than *extend_size*, e.g., if a + linear-time amortized behavior over a long sequence of appends() + is wanted. (for example, list and array.array do this). + + :param extend_by_template: Create a sequence using sequence_factory + that is used as template for extending the wrapped sequence + base on this template collection. Typically used if the collection + is an array, because collection.extend(repeat(, )) + requires unboxing each value, while collection.extend(template) + just copies the native values. """ def __init__( @@ -654,6 +696,7 @@ def __init__( ], default: T_default_value, extend_size: int, + extend_by_template: bool, ) -> None: self._sequence_factory: Callable[ [], @@ -670,6 +713,23 @@ def __init__( ] = self._sequence_factory() self._default = default self._extend_size = extend_size + self._extend_by_template = extend_by_template + + # If demanded, create an empty collection that can be used as + # template for extending the wrapped collection + expansion_template: Optional[ + SequenceForGearProto[ + NonNegativeDenseInt, + Union[T_value, T_default_value], + Union[T_value, T_default_value], + ] + ] + if extend_by_template: + expansion_template = sequence_factory() + expansion_template.extend(repeat(default, extend_size)) + else: + expansion_template = None + self._expansion_template = expansion_template def sequence( self, @@ -688,8 +748,8 @@ def default(self) -> T_default_value: def extend_and_set(self, key: NonNegativeDenseInt, value: T_value) -> None: """Extend the underlying sequence in order to store *value* for *key*. - For new keys "below" *key* and, for better overall performance, also - for many keys "above" *key*, the *default* value is stored. + For new keys "below" *key* and, for better overall performance, maybe also + for some keys "above" *key*, the *default* value is stored. The method is called by NoGraphs if writing to the underlying sequence raised an IndexError for some key. It allows NoGraphs to handle such @@ -698,11 +758,21 @@ def extend_and_set(self, key: NonNegativeDenseInt, value: T_value) -> None: call to the method is not as performance critical as the direct accesses to the underlying sequence. """ - default = self._default collection = self._sequence - collection.extend(repeat(default, key - len(collection))) - collection.append(value) - collection.extend(repeat(default, self._extend_size)) + extend_size = self._extend_size + target_len = (key + 1) + extend_size + expansion_template = self._expansion_template + if expansion_template is None: + # The following is typically fast and does not require an + # expansion template + collection.extend(repeat(self._default, target_len - len(collection))) + else: + # For collections with native (unboxed) elements, the following is + # better, because it avoids unboxing each element for storing it + collection_extend = collection.extend + while len(collection) < target_len: + collection_extend(expansion_template) + collection[key] = value # --- Implementations of VertexSet(...ByWrapper) and @@ -718,9 +788,14 @@ class VertexSetWrappingSequence( """A VertexSequenceWrapper that emulates a VertexSet for non-negative dense integer keys based on an underlying SequenceForGearProto. - It must be initialized with a factory function for the SequenceForGearProto - to be wrapped, the number of elements the sequence should be extended when - more space is needed, and an iterable of keys with initial content. + It must be initialized with + a factory function for the SequenceForGearProto to be wrapped, + the number of elements the sequence should be extended when + more space is needed, + a bool flag that controls if an extension template + should be used to extend the sequence + (see *VertexSequenceWrapper* for details), + and an iterable of keys with initial content. In performance critical cases, NoGraphs does not call the methods of this class (emulation of a set), but directly accesses the underlying sequence. @@ -732,9 +807,10 @@ def __init__( [], SequenceForGearProto[NonNegativeDenseInt, int, int] ], extend_size: int, + extend_by_template: bool, keys: Iterable[NonNegativeDenseInt], ): - super().__init__(sequence_factory, 0, extend_size) + super().__init__(sequence_factory, 0, extend_size, extend_by_template) self.update_from_keys(keys) def __repr__(self) -> str: @@ -746,13 +822,13 @@ def __iter__(self) -> Iterator[NonNegativeDenseInt]: """Return an iterator that iterates the "officially" contained keys. Keys with *default* as value are omitted and interpreted as key gap. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover # Not reachable @abstractmethod def __len__(self) -> int: """Return number of keys stored in the collection. Time complexity of O(n), needs to iterate through the collection.""" - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover # Not reachable @abstractmethod def _from_iterable( @@ -763,12 +839,12 @@ def _from_iterable( See section "Examples and Recipes" of collections.abc about the Set mixin. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover # Not reachable @abstractmethod def update_from_keys(self, keys: Iterable[NonNegativeDenseInt]) -> None: """Add all the keys to the collection""" - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover # Not reachable @abstractmethod def index_and_bit_method( @@ -779,7 +855,7 @@ def index_and_bit_method( """For cases where a sequence of bytes is used to emulate a bit array: return function that computes the byte index and the bit number for *key* """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover # Not reachable class VertexSetWrappingSequenceNoBitPacking(VertexSetWrappingSequence): @@ -831,7 +907,7 @@ def _from_iterable( # sorted values, but there is no guarantee. So, we do not assume this. # So, we create an empty sequence and add our values to it. new_collection = VertexSetWrappingSequenceNoBitPacking( - self._sequence_factory, self._extend_size, () + self._sequence_factory, self._extend_size, self._extend_by_template, () ) new_collection.update_from_keys(elements) return new_collection @@ -910,7 +986,7 @@ def _from_iterable( # sorted values, but there is no guarantee. So, we do not assume this. # So, we create an empty sequence and add our values to it. new_collection = VertexSetWrappingSequenceBitPacking( - self._sequence_factory, self._extend_size, () + self._sequence_factory, self._extend_size, self._extend_by_template, () ) new_collection.update_from_keys(elements) return new_collection @@ -928,11 +1004,15 @@ class VertexMappingWrappingSequence(VertexSequenceWrapper[T_value, T_default_val """A VertexSequenceSwapper that emulates a VertexMapping for non-negative dense integer keys based on an underlying SequenceForGearProto. - It must be initialized with a factory function for the SequenceForGearProto - to be wrapped, the number of elements the sequence should be extended when - more space is needed, + It must be initialized with + a factory function for the SequenceForGearProto to be wrapped, a default value of type T_value that is suitable for the use case, - and an iterable of keys with initial content. + the number of elements the sequence should be extended when + more space is needed, + a bool flag that controls if an extension template + should be used to extend the sequence + (see *VertexSequenceWrapper* for details), + and an iterable of keys and values with initial content. In performance critical cases, NoGraphs does not call the methods of this class (emulation of a set), but directly accesses the underlying sequence. @@ -950,9 +1030,10 @@ def __init__( ], default: T_default_value, extend_size: int, + extend_by_template: bool, items: Iterable[tuple[NonNegativeDenseInt, T_value]], ): - super().__init__(sequence_factory, default, extend_size) + super().__init__(sequence_factory, default, extend_size, extend_by_template) self.update_from_keys_values(items) def __getitem__(self, key: NonNegativeDenseInt) -> T_value: diff --git a/src/nographs/_gears.py b/src/nographs/_gears.py index ae5b213..c7480b9 100644 --- a/src/nographs/_gears.py +++ b/src/nographs/_gears.py @@ -1,14 +1,14 @@ -from __future__ import annotations -import collections from collections.abc import ( Iterable, MutableSequence, + Callable, ) from typing import ( Protocol, Literal, Generic, Union, + Optional, ) # Sphinx has an issue with documenting tuple[T] in HTML. Thus, for @@ -128,19 +128,24 @@ class based on a wrapped sequence, NoGraphs directly accesses the sequence for def max_value_for_integer_array_type_code( - c: Literal["b", "B", "h", "H", "i", "I", "l", "L", "q", "Q"] + c: Literal["b", "B", "h", "H", "i", "I", "l", "L", "q", "Q"], ) -> int: """Highest value than can be stores in an array of this type""" bytes_of_type_code = {"b": 1, "h": 2, "i": 2, "l": 4, "q": 8}[c.lower()] bits = bytes_of_type_code * 8 if c.lower() == c: bits -= 1 - return 2**bits - 1 + res: int = 2**bits - 1 + return res # -- Gear protocols -- +# Todo: Collection factories should be replaceable by attribute, not by subclassing. +# Todo: Tutorial: Section "Defining your own gear" currently does not work! + + class GearWithoutDistances(Protocol[T_vertex, T_vertex_id, T_labels]): """Protocol for a feature-limited kind of gear that offers collections that can store vertices, vertex_ids and edge data, but no edge @@ -155,7 +160,7 @@ def vertex_id_set( :param initial_content: The collection is created with this initial content. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable @abstractmethod def vertex_id_to_vertex_mapping( @@ -165,7 +170,7 @@ def vertex_id_to_vertex_mapping( :param initial_content: The collection is created with this initial content. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable @abstractmethod def vertex_id_to_edge_labels_mapping( @@ -175,21 +180,21 @@ def vertex_id_to_edge_labels_mapping( :param initial_content: The collection is created with this initial content. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable @abstractmethod def sequence_of_vertices( self, initial_content: Iterable[T_vertex] ) -> MutableSequenceOfVertices[T_vertex]: """Factory for a sequence of vertices.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable @abstractmethod def sequence_of_edge_labels( self, initial_content: Iterable[T_labels] ) -> MutableSequenceOfLabels[T_labels]: """Factory for a sequence of edge attributes.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable @abstractmethod def vertex_id_to_number_mapping( @@ -203,7 +208,7 @@ def vertex_id_to_number_mapping( :param initial_content: The collection is created with this initial content. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable class Gear( @@ -246,13 +251,13 @@ class Gear( @abstractmethod def zero(self) -> T_weight: """Return the zero value of T_weight, e.g., 0.0 for float.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable @abstractmethod def infinity(self) -> T_weight: """Return the positive infinity value of T_weight for the gear, e.g., float("infinity") for float.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable @abstractmethod def vertex_id_to_distance_mapping( @@ -265,7 +270,7 @@ def vertex_id_to_distance_mapping( :param initial_content: The collection is created with this initial content. """ - raise NotImplementedError + raise NotImplementedError # pragma: no cover # Not reachable def raise_distance_infinity_overflow_error(self, value: T_weight) -> None: """Report that the computed value is equal or larger than the chosen @@ -283,7 +288,29 @@ def raise_distance_infinity_overflow_error(self, value: T_weight) -> None: # --- Hashable vertex IDs (hashable vertices or vertex_to_id returns hashable --- -class DefaultdictWithNiceStr(collections.defaultdict[T_vertex_id, T_weight]): +# # MyPyC: defaultdict cannot be subclassed as native class. See next class... +# class DefaultdictWithNiceStr(collections.defaultdict[T_vertex_id, T_weight]): +# def __str__(self) -> str: +# return str(dict(self)) + + +class DefaultdictWithNiceStr(dict[T_vertex_id, T_weight]): + """Similar to a defaultdict, but prints itself like a dict. And the + default_factory is not allowed to return None. + """ + + # todo: Use this class only for MyPyC, and original above for source wheel + def __init__( + self, + default_factory: Callable[[], T_weight], + initial_content: Iterable[tuple[T_vertex_id, T_weight]], + ) -> None: + self._default_factory = default_factory + super().__init__(initial_content) + + def __missing__(self, key: T_vertex_id) -> Optional[T_weight]: + return self._default_factory() + def __str__(self) -> str: return str(dict(self)) @@ -513,11 +540,13 @@ def vertex_id_set( ) extend_size = 1024 if self._no_bit_packing else 1024 // 8 if self._no_arrays: + extend_by_template = False def sequence_factory() -> SequenceForGearProto[int, int, int]: return [0] * pre_allocate else: + extend_by_template = True def sequence_factory() -> SequenceForGearProto[int, int, int]: return array("B", repeat(0, pre_allocate)) @@ -527,20 +556,22 @@ def sequence_factory() -> SequenceForGearProto[int, int, int]: if self._no_bit_packing else VertexSetWrappingSequenceBitPacking ) - return collection_class(sequence_factory, extend_size, initial_content) + return collection_class( + sequence_factory, extend_size, extend_by_template, initial_content + ) def vertex_id_to_vertex_mapping( self, initial_content: Iterable[Tuple[IntVertexID, T_vertex]] ) -> VertexMapping[IntVertexID, T_vertex]: return VertexMappingWrappingSequenceWithNone[T_vertex]( - lambda: [None] * self._pre_allocate, None, 1024, initial_content + lambda: [None] * self._pre_allocate, None, 1024, False, initial_content ) def vertex_id_to_edge_labels_mapping( self, initial_content: Iterable[Tuple[IntVertexID, T_labels]] ) -> VertexMapping[IntVertexID, T_labels]: return VertexMappingWrappingSequenceWithNone[T_labels]( - lambda: [None] * self._pre_allocate, None, 1024, initial_content + lambda: [None] * self._pre_allocate, None, 1024, False, initial_content ) def sequence_of_vertices( @@ -566,6 +597,7 @@ def vertex_id_to_distance_mapping( lambda: [self._infinity_value] * self._pre_allocate, self._infinity_value, 1024, + False, initial_content, ) @@ -581,6 +613,7 @@ def vertex_id_to_number_mapping( ), 0, 1024, + True, initial_content, ) @@ -676,6 +709,7 @@ def vertex_id_to_distance_mapping( ), self._infinity_value, 1024, + True, initial_content, ) @@ -724,6 +758,7 @@ def vertex_id_to_distance_mapping( ), self._infinity_value, 1024, + True, initial_content, ) @@ -790,6 +825,7 @@ def vertex_id_to_vertex_mapping( ), max_vertex_type_value, 1024, + True, initial_content, ) @@ -901,6 +937,7 @@ def vertex_id_to_distance_mapping( ), self._infinity_value, 1024, + True, initial_content, ) @@ -956,5 +993,6 @@ def vertex_id_to_distance_mapping( ), self._infinity_value, 1024, + True, initial_content, ) diff --git a/src/nographs/_path.py b/src/nographs/_path.py index 6ee0bf9..c48cea0 100644 --- a/src/nographs/_path.py +++ b/src/nographs/_path.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import itertools from collections.abc import Iterator, Callable from typing import Generic, Any, Union, TypeVar @@ -40,9 +38,10 @@ def _get_empty_iter() -> Iterator[Any]: def reverse_edges( - edges: Iterator[UnweightedLabeledFullEdge[T_vertex, T_labels]] + edges: Iterator[UnweightedLabeledFullEdge[T_vertex, T_labels]], ) -> Iterator[UnweightedLabeledFullEdge[T_vertex, T_labels]]: - return ((w, v, l) for v, w, l in edges) + for v, w, l in edges: + yield w, v, l SelfPath = TypeVar("SelfPath", bound="Path[Any, Any, Any]") @@ -156,11 +155,11 @@ def get_edge_backwards_iter() -> ( ) @classmethod - def of_nothing(cls: type[SelfPath]) -> Path[T_vertex, T_vertex_id, T_labels]: + def of_nothing(cls: type[SelfPath]) -> "Path[T_vertex, T_vertex_id, T_labels]": return cls(_get_empty_iter, _get_empty_iter, _get_empty_iter, _get_empty_iter) @classmethod - def from_vertex(cls, vertex: T_vertex) -> Path[T_vertex, T_vertex_id, T_labels]: + def from_vertex(cls, vertex: T_vertex) -> "Path[T_vertex, T_vertex_id, T_labels]": def get_iter_of_one_vertex() -> Iterator[T_vertex]: return iter((vertex,)) @@ -185,7 +184,8 @@ def iter_edges_from_start(self) -> Iterator[UnweightedUnlabeledFullEdge[T_vertex def iter_edges_to_start(self) -> Iterator[UnweightedUnlabeledFullEdge[T_vertex]]: """Iterate the edges of the path from the last to the first.""" - return ((v, w) for w, v in pairwise(self._get_vertex_backwards_iter())) + for w, v in pairwise(self._get_vertex_backwards_iter()): + yield v, w def iter_labeled_edges_to_start( self, diff --git a/src/nographs/_paths.py b/src/nographs/_paths.py index 9d9a0d3..d74138a 100644 --- a/src/nographs/_paths.py +++ b/src/nographs/_paths.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from collections.abc import Sequence, Iterator, MutableMapping from typing import Optional, Any, Generic, cast, Union from abc import ABC, abstractmethod @@ -412,7 +410,10 @@ def __init__( ) = access_to_vertex_mapping_expect_none(labels) def append_edge( - self, from_vertex: T_vertex, to_vertex_id: T_vertex_id, to_edge: LabeledOutEdge + self, + from_vertex: T_vertex, + to_vertex_id: T_vertex_id, + to_edge: LabeledOutEdge[T_vertex, Any, T_labels], ) -> None: """Create a new path that starts with the existing path to from_vertex and ends with the given vertex (resp. id). The additional diff --git a/src/nographs/_strategies/__init__.py b/src/nographs/_strategies/__init__.py index 6ca027d..683450e 100644 --- a/src/nographs/_strategies/__init__.py +++ b/src/nographs/_strategies/__init__.py @@ -1,6 +1,10 @@ -from .strategy import Strategy +# In the following, we use absolute imports starting with +# "nographs._strategies." instead of just "." because: +# MyPyC: https://github.com/mypyc/mypyc/issues/996 -from .type_aliases import ( +from nographs._strategies.strategy import Strategy + +from nographs._strategies.type_aliases import ( T_strategy, NextVertices, NextEdges, @@ -14,46 +18,49 @@ BNextWeightedLabeledEdges, ) -from .traversals.traversal import Traversal +from nographs._strategies.traversals.traversal import Traversal -from .traversals.without_weights.breadth_first import ( +from nographs._strategies.traversals.without_weights.breadth_first import ( TraversalBreadthFirst, TraversalBreadthFirstFlex, ) -from .traversals.without_weights.depth_first import ( +from nographs._strategies.traversals.without_weights.depth_first import ( TraversalDepthFirst, TraversalDepthFirstFlex, DFSEvent, DFSMode, ) -from .traversals.without_weights.neighbors_then_depth import ( +from nographs._strategies.traversals.without_weights.neighbors_then_depth import ( TraversalNeighborsThenDepth, TraversalNeighborsThenDepthFlex, ) -from .traversals.without_weights.topological_sort import ( +from nographs._strategies.traversals.without_weights.topological_sort import ( TraversalTopologicalSort, TraversalTopologicalSortFlex, ) -from .traversals.with_weights.shortest_paths import ( +from nographs._strategies.traversals.with_weights.shortest_paths import ( TraversalShortestPaths, TraversalShortestPathsFlex, ) -from .traversals.with_weights.a_star import TraversalAStar, TraversalAStarFlex -from .traversals.with_weights.minimum_spanning_tree import ( +from nographs._strategies.traversals.with_weights.a_star import ( + TraversalAStar, + TraversalAStarFlex, +) +from nographs._strategies.traversals.with_weights.minimum_spanning_tree import ( TraversalMinimumSpanningTree, TraversalMinimumSpanningTreeFlex, ) -from .traversals.with_weights.extra_infinite_branching import ( +from nographs._strategies.traversals.with_weights.extra_infinite_branching import ( TraversalShortestPathsInfBranchingSortedFlex, TraversalShortestPathsInfBranchingSorted, ) -from .bidirectional_search.breadth_first import ( +from nographs._strategies.bidirectional_search.breadth_first import ( BSearchBreadthFirst, BSearchBreadthFirstFlex, ) -from .bidirectional_search.shortest_path import ( +from nographs._strategies.bidirectional_search.shortest_path import ( BSearchShortestPath, BSearchShortestPathFlex, ) diff --git a/src/nographs/_strategies/bidirectional_search/base.py b/src/nographs/_strategies/bidirectional_search/base.py index 2b69252..67913b4 100644 --- a/src/nographs/_strategies/bidirectional_search/base.py +++ b/src/nographs/_strategies/bidirectional_search/base.py @@ -1,6 +1,4 @@ -from __future__ import annotations - -from typing import Optional, Any +from typing import Optional, Any, Union from nographs._types import ( T_vertex, @@ -22,7 +20,10 @@ # --------------- internal support functions ------------- -def _search_needs_search_object(obj: Any, needed_class: type) -> None: +# Cython needs Any here, because if needed_class is a subclass of ABCMeta, +# it does not recognize that this is a type, but only that it is such a subclass. +# MyPy is fine with that, because the signature of isinstance is only roughly typed. +def _search_needs_search_object(obj: Any, needed_class: Union[type, Any]) -> None: if not isinstance(obj, needed_class): raise RuntimeError( "Method start_from can only be called on a search strategy object." diff --git a/src/nographs/_strategies/bidirectional_search/breadth_first.py b/src/nographs/_strategies/bidirectional_search/breadth_first.py index 72030bc..d2a51a5 100644 --- a/src/nographs/_strategies/bidirectional_search/breadth_first.py +++ b/src/nographs/_strategies/bidirectional_search/breadth_first.py @@ -1,7 +1,5 @@ -from __future__ import annotations - import itertools -from typing import Optional, Iterable, Generic +from typing import Optional, Iterable, Generic, ClassVar from nographs._types import ( T_vertex, @@ -77,6 +75,8 @@ class BSearchBreadthFirstFlex(Strategy[T_vertex, T_vertex_id, T_labels]): TraversalBreadthFirst().start_at(v).go_to(v) fails. """ + _state_attrs: ClassVar = Strategy._state_attrs + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], @@ -91,14 +91,14 @@ def __init__( BNextEdges[ T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], - ], + ] ] = None, next_labeled_edges: Optional[ BNextLabeledEdges[ T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], T_labels, - ], + ] ] = None, ) -> None: self._vertex_to_id = vertex_to_id @@ -224,7 +224,7 @@ def start_from( # ----- Initialize method specific bookkeeping ----- # Get the right class for storing a path (labeled or not) - path_cls: type[Path] + path_cls: type[Path[T_vertex, T_vertex_id, T_labels]] if self._labeled_edges: path_cls = PathOfLabeledEdges[T_vertex, T_vertex_id, T_labels] else: @@ -380,14 +380,14 @@ def __init__( BNextEdges[ T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], - ], + ] ] = None, next_labeled_edges: Optional[ BNextLabeledEdges[ T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], T_labels, - ], + ] ] = None, ) -> None: super().__init__( diff --git a/src/nographs/_strategies/bidirectional_search/shortest_path.py b/src/nographs/_strategies/bidirectional_search/shortest_path.py index 1725199..953d545 100644 --- a/src/nographs/_strategies/bidirectional_search/shortest_path.py +++ b/src/nographs/_strategies/bidirectional_search/shortest_path.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import itertools from heapq import heapify, heappop, heappush -from typing import Generic, Optional, Union +from typing import Generic, Optional, Union, ClassVar from collections.abc import Iterable, Collection from nographs._types import ( @@ -81,6 +79,8 @@ class BSearchShortestPathFlex( TraversalShortestPaths().start_at(v).go_to(v) fails. """ + _state_attrs: ClassVar = Strategy._state_attrs + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], @@ -88,18 +88,18 @@ def __init__( next_edges: Optional[ BNextWeightedEdges[ T_vertex, - BSearchShortestPathFlex[T_vertex, T_vertex_id, T_weight, T_labels], + "BSearchShortestPathFlex[T_vertex, T_vertex_id, T_weight, T_labels]", T_weight, - ], + ] ] = None, *, next_labeled_edges: Optional[ BNextWeightedLabeledEdges[ T_vertex, - BSearchShortestPathFlex[T_vertex, T_vertex_id, T_weight, T_labels], + "BSearchShortestPathFlex[T_vertex, T_vertex_id, T_weight, T_labels]", T_weight, T_labels, - ], + ] ] = None, ) -> None: self._vertex_to_id = vertex_to_id @@ -262,7 +262,7 @@ def start_from( ) # Get the right class for storing a path (labeled or not) - path_cls: type[Path] + path_cls: type[Path[T_vertex, T_vertex_id, T_labels]] if labeled_edges: path_cls = PathOfLabeledEdges[T_vertex, T_vertex_id, T_labels] else: @@ -510,7 +510,9 @@ def start_from( @staticmethod def _search_failed( - path_cls: type[Path], infinity: T_weight, fail_silently: bool + path_cls: type[Path[T_vertex, T_vertex_id, T_labels]], + infinity: T_weight, + fail_silently: bool, ) -> tuple[T_weight, Path[T_vertex, T_vertex_id, T_labels]]: """For a silent fail, return the value that marks that the search has failed: A tuple of the infinity value of the weights and an empty path. If no @@ -558,7 +560,7 @@ def __init__( T_vertex, T_vertex, Union[T_weight, float], T_labels ], T_weight, - ], + ] ] = None, *, next_labeled_edges: Optional[ @@ -569,7 +571,7 @@ def __init__( ], T_weight, T_labels, - ], + ] ] = None, ) -> None: super().__init__( diff --git a/src/nographs/_strategies/strategy.py b/src/nographs/_strategies/strategy.py index 917fb80..33202fa 100644 --- a/src/nographs/_strategies/strategy.py +++ b/src/nographs/_strategies/strategy.py @@ -1,7 +1,5 @@ -from __future__ import annotations - from abc import ABC -from typing import Generic, Any, Optional, Iterable +from typing import Generic, Any, Optional, Iterable, ClassVar from nographs._types import T_vertex, T_vertex_id, T_labels @@ -9,6 +7,13 @@ class Strategy(ABC, Generic[T_vertex, T_vertex_id, T_labels]): """Base class of the traversal strategies and search strategies of NoGraphs.""" + _state_attrs: ClassVar = list[str]() + # Public, writable attributes of the class. See _compute_state_attrs(). + + _state_attrs_checked: ClassVar = False + # Marks if results of _compute_state_attrs have already been checked + # when not running under MyPyC. + def _improve_state( self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None ) -> None: @@ -33,9 +38,9 @@ def state_to_str(self, vertices: Optional[Iterable[T_vertex]] = None) -> str: Implementation details, not covered by the semantic versioning: Currently, the method aims at providing a uniform behaviour over different - platforms (*CPython* and *PyPy*) and collection types (Gears with different - *MutableSet* and *MutableMapping* implementations). It behaves roughly as - follows: + platforms (*CPython*, *PyPy*, and *MyPyC*) and collection types (Gears with + different *MutableSet* and *MutableMapping* implementations). It behaves + roughly as follows: - A *MutableSet*, e.g. attribute *visited*, is described similar to a *set*, but items are sorted lexicographically in their string representations @@ -51,6 +56,32 @@ def state_to_str(self, vertices: Optional[Iterable[T_vertex]] = None) -> str: :param vertices: If the strategy can provide additional state data w.r.t. these vertices, it will do so. """ + # Compute the state + # When compiled with MyPyC, the following computes an empty + # list even for subclasses with attributes. state = dict((k, v) for k, v in self.__dict__.items() if k[0] != "_") + # print(">>>state_to_str", file=sys.stderr) + if len(self.__dict__.keys()): # pragma: no cover # Not reachable on MyPy + # __dict__ is filled. So, we are not on MyPyC compiled code. + # Now, we check, whether _compute_state_attrs works correctly, + # if not done before. + if not self._state_attrs_checked: + type(self)._state_attrs_checked = True + state_attrs = self._state_attrs + if list(state.keys()) != state_attrs: + raise RuntimeError( + "Internal error: attributes do not match" + + ".\nClass:" + + self.__class__.__name__ + + ".\nManual list: " + + str(state_attrs) + + ".\nKeys from __dict__: " + + str(list(state.keys())) + ) + else: # pragma: no cover # Only reachable on MyPy + # We rely on self._compute_state_attrs for getting the state. + state = {k: getattr(self, k) for k in self._state_attrs} + + # Compute the optimal human-readable representation of the state attributes self._improve_state(state, vertices) return str(state) diff --git a/src/nographs/_strategies/traversals/traversal.py b/src/nographs/_strategies/traversals/traversal.py index fe4587f..3ca6809 100644 --- a/src/nographs/_strategies/traversals/traversal.py +++ b/src/nographs/_strategies/traversals/traversal.py @@ -1,6 +1,4 @@ -""" Traversal strategies for unweighted graphs with or without edge labels """ - -from __future__ import annotations +"""Traversal strategies for unweighted graphs with or without edge labels""" from abc import abstractmethod from collections.abc import ( @@ -9,7 +7,7 @@ Generator, Collection, ) -from typing import Optional, Any, cast, overload, Literal +from typing import Optional, Any, cast, overload, Literal, ClassVar from nographs._gears import ( GearWithoutDistances, @@ -43,16 +41,21 @@ def no_generator() -> Generator[Any, None, None]: """ raise RuntimeError("Traversal not started, iteration not possible") # noinspection PyUnreachableCode - yield None + yield None # type: ignore[unreachable] -class Traversal(Strategy[T_vertex, T_vertex_id, T_labels], Iterable): +class Traversal(Strategy[T_vertex, T_vertex_id, T_labels], Iterable[T_vertex]): """ Abstract Class. Its subclasses provide methods to iterate through vertices and edges using some specific traversal strategies. """ - @abstractmethod + _state_attrs: ClassVar = Strategy._state_attrs + ["paths"] + + # MyPyC: @abstractmethod commented out in order to avoid the following error: + # error C2198: "CPyDef_traversal___Traversal_____init__": + # Nicht genügend Argumente für Aufruf. + # @abstractmethod def __init__( self, labeled_edges: bool, diff --git a/src/nographs/_strategies/traversals/with_weights/__init__.py b/src/nographs/_strategies/traversals/with_weights/__init__.py index 88a9cce..c11045d 100644 --- a/src/nographs/_strategies/traversals/with_weights/__init__.py +++ b/src/nographs/_strategies/traversals/with_weights/__init__.py @@ -1 +1 @@ -""" Traversal strategies for weighted edges """ +"""Traversal strategies for weighted edges""" diff --git a/src/nographs/_strategies/traversals/with_weights/a_star.py b/src/nographs/_strategies/traversals/with_weights/a_star.py index e525b54..03317b5 100644 --- a/src/nographs/_strategies/traversals/with_weights/a_star.py +++ b/src/nographs/_strategies/traversals/with_weights/a_star.py @@ -1,9 +1,6 @@ -from __future__ import annotations - import itertools from heapq import heapify, heappop, heappush -from numbers import Real -from typing import Optional, Any, Generic, Union +from typing import Optional, Any, Generic, Union, ClassVar from collections.abc import Callable, Iterable, Generator from nographs._types import ( @@ -84,6 +81,11 @@ class TraversalAStarFlex( *path_length*, *depth*, *paths*. """ + _state_attrs: ClassVar = _TraversalWithDistances._state_attrs + [ + "path_length", + "depth", + ] + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], @@ -91,7 +93,9 @@ def __init__( next_edges: Optional[ NextWeightedEdges[ T_vertex, - TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels], + """TraversalAStarFlex[ + T_vertex, T_vertex_id, T_weight, T_labels + ]""", T_weight, ] ] = None, @@ -99,7 +103,9 @@ def __init__( next_labeled_edges: Optional[ NextWeightedLabeledEdges[ T_vertex, - TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels], + """TraversalAStarFlex[ + T_vertex, T_vertex_id, T_weight, T_labels + ]""", T_weight, T_labels, ] @@ -126,7 +132,7 @@ def __init__( """ self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() - self._heuristic: Optional[Callable[[T_vertex], Real]] = None + self._heuristic: Optional[Callable[[T_vertex], T_weight]] = None self._known_distances: Optional[ VertexIdToDistanceMapping[T_vertex_id, T_weight] ] = None @@ -139,7 +145,7 @@ def __init__( def start_from( self, - heuristic: Callable[[T_vertex], Real], + heuristic: Callable[[T_vertex], T_weight], start_vertex: Optional[T_vertex] = None, *, start_vertices: Optional[Iterable[T_vertex]] = None, @@ -151,7 +157,7 @@ def start_from( known_path_length_guesses: Optional[ VertexIdToDistanceMapping[T_vertex_id, T_weight] ] = None, - ) -> TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + ) -> "TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. @@ -456,6 +462,8 @@ class TraversalAStar( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = TraversalAStarFlex._state_attrs + def __init__( self, next_edges: Optional[ diff --git a/src/nographs/_strategies/traversals/with_weights/extra_infinite_branching.py b/src/nographs/_strategies/traversals/with_weights/extra_infinite_branching.py index effa77c..d7e82cd 100644 --- a/src/nographs/_strategies/traversals/with_weights/extra_infinite_branching.py +++ b/src/nographs/_strategies/traversals/with_weights/extra_infinite_branching.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Any, Union, Generic, Optional from collections.abc import Iterable, Iterator, Generator from abc import ABC @@ -102,7 +100,7 @@ def __init__( internal_gear: Gear[State[T_vertex_id], State[T_vertex_id], T_weight, Any], next_edges: NextWeightedEdges[ T_vertex_id, - TraversalShortestPathsInfBranchingSortedFlex[T_vertex_id, T_weight], + "TraversalShortestPathsInfBranchingSortedFlex[T_vertex_id, T_weight]", T_weight, ], ) -> None: @@ -129,7 +127,7 @@ def start_from( build_paths: bool = False, combined_calculation_limit: Optional[int] = None, store_distances: bool = False, - ) -> TraversalShortestPathsInfBranchingSortedFlex[T_vertex_id, T_weight]: + ) -> "TraversalShortestPathsInfBranchingSortedFlex[T_vertex_id, T_weight]": """ Start the traversal at a vertex or a set of vertices and set parameters. diff --git a/src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py b/src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py index 9a2f8cc..037e3b8 100644 --- a/src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py +++ b/src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import itertools from heapq import heapify, heappop, heappush -from typing import Optional, Any, Generic, Union +from typing import Optional, Any, Generic, Union, ClassVar from collections.abc import Iterable, Generator from nographs._types import ( @@ -69,6 +67,8 @@ class TraversalMinimumSpanningTreeFlex( *edge* and *paths*. """ + _state_attrs: ClassVar = _TraversalWithWeights._state_attrs + ["edge"] + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], @@ -76,9 +76,9 @@ def __init__( next_edges: Optional[ NextWeightedEdges[ T_vertex, - TraversalMinimumSpanningTreeFlex[ + """TraversalMinimumSpanningTreeFlex[ T_vertex, T_vertex_id, T_weight, T_labels - ], + ]""", T_weight, ] ] = None, @@ -86,9 +86,9 @@ def __init__( next_labeled_edges: Optional[ NextWeightedLabeledEdges[ T_vertex, - TraversalMinimumSpanningTreeFlex[ + """TraversalMinimumSpanningTreeFlex[ T_vertex, T_vertex_id, T_weight, T_labels - ], + ]""", T_weight, T_labels, ] @@ -115,7 +115,7 @@ def start_from( start_vertices: Optional[Iterable[T_vertex]] = None, build_paths: bool = False, calculation_limit: Optional[int] = None, - ) -> TraversalMinimumSpanningTreeFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + ) -> "TraversalMinimumSpanningTreeFlex[T_vertex, T_vertex_id, T_weight, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. If you provide more than one start vertex, the result consists of several @@ -380,6 +380,8 @@ class TraversalMinimumSpanningTree( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = TraversalMinimumSpanningTreeFlex._state_attrs + def __init__( self, next_edges: Optional[ diff --git a/src/nographs/_strategies/traversals/with_weights/shortest_paths.py b/src/nographs/_strategies/traversals/with_weights/shortest_paths.py index e2cb040..9225404 100644 --- a/src/nographs/_strategies/traversals/with_weights/shortest_paths.py +++ b/src/nographs/_strategies/traversals/with_weights/shortest_paths.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import itertools from heapq import heapify, heappop, heappush -from typing import Optional, Any, Generic, Union +from typing import Optional, Any, Generic, Union, ClassVar from collections.abc import Iterable, Generator from nographs._gears import VertexIdToDistanceMapping @@ -70,6 +68,8 @@ class TraversalShortestPathsFlex( *distance*, *depth*, *paths*, and *distances*. """ + _state_attrs: ClassVar = _TraversalWithDistance._state_attrs + ["depth"] + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], @@ -77,7 +77,9 @@ def __init__( next_edges: Optional[ NextWeightedEdges[ T_vertex, - TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels], + """TraversalShortestPathsFlex[ + T_vertex, T_vertex_id, T_weight, T_labels + ]""", T_weight, ] ] = None, @@ -85,7 +87,9 @@ def __init__( next_labeled_edges: Optional[ NextWeightedLabeledEdges[ T_vertex, - TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels], + """TraversalShortestPathsFlex[ + T_vertex, T_vertex_id, T_weight, T_labels + ]""", T_weight, T_labels, ] @@ -126,7 +130,7 @@ def start_from( known_distances: Optional[ VertexIdToDistanceMapping[T_vertex_id, T_weight] ] = None, - ) -> TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + ) -> "TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. @@ -395,6 +399,8 @@ class TraversalShortestPaths( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = TraversalShortestPathsFlex._state_attrs + def __init__( self, next_edges: Optional[ diff --git a/src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py b/src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py index 726ee2d..5ffe1fe 100644 --- a/src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py +++ b/src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py @@ -1,8 +1,5 @@ -from __future__ import annotations - from abc import ABC -from numbers import Real -from typing import Generic, Any, Optional, Iterable, Iterator +from typing import Generic, Any, Optional, Iterable, Iterator, ClassVar from nographs._types import ( T_vertex, @@ -60,6 +57,8 @@ class _TraversalWithWeights( ): """A Traversal that needs weighted edges and uses a gear suitable for this.""" + _state_attrs: ClassVar = Traversal._state_attrs + def __init__( self, labeled_edges: bool, @@ -80,6 +79,8 @@ class _TraversalWithDistances( its state. """ + _state_attrs: ClassVar = _TraversalWithWeights._state_attrs + ["distances"] + def __init__( self, labeled_edges: bool, @@ -126,6 +127,8 @@ class _TraversalWithDistance( If offers the go_for_distance_range method based on the distance. """ + _state_attrs: ClassVar = _TraversalWithDistances._state_attrs + ["distance"] + def __init__( self, labeled_edges: bool, @@ -142,7 +145,9 @@ def __init__( start vertex to the visited vertex """ - def go_for_distance_range(self, start: Real, stop: Real) -> Iterator[T_vertex]: + def go_for_distance_range( + self, start: T_weight, stop: T_weight + ) -> Iterator[T_vertex]: """ For a started traversal, return an iterator. During the traversal, the iterator skips vertices as long as their distance is lower than *start*. diff --git a/src/nographs/_strategies/traversals/without_weights/__init__.py b/src/nographs/_strategies/traversals/without_weights/__init__.py index b37ba2d..a50700f 100644 --- a/src/nographs/_strategies/traversals/without_weights/__init__.py +++ b/src/nographs/_strategies/traversals/without_weights/__init__.py @@ -1 +1 @@ -""" Traversal strategies for unweighted edges """ +"""Traversal strategies for unweighted edges""" diff --git a/src/nographs/_strategies/traversals/without_weights/breadth_first.py b/src/nographs/_strategies/traversals/without_weights/breadth_first.py index ccee6d2..c407ae8 100644 --- a/src/nographs/_strategies/traversals/without_weights/breadth_first.py +++ b/src/nographs/_strategies/traversals/without_weights/breadth_first.py @@ -1,7 +1,4 @@ -from __future__ import annotations - -import copy -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Iterator, Generator @@ -84,25 +81,29 @@ class TraversalBreadthFirstFlex( *depth*, *paths*, and *visited*. """ + _state_attrs: ClassVar = _TraversalWithoutWeightsWithVisited._state_attrs + [ + "depth" + ] + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], next_vertices: Optional[ NextVertices[ - T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] + T_vertex, "TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]" ] ] = None, *, next_edges: Optional[ NextEdges[ - T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] + T_vertex, "TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]" ] ] = None, next_labeled_edges: Optional[ NextLabeledEdges[ T_vertex, - TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], + "TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]", T_labels, ] ] = None, @@ -128,6 +129,37 @@ def __init__( """ self._report_depth_increase = False + def _copy( + self, + ) -> "TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]": + """Return a copy of the current traversal. + + (We cannot use copy.copy() instead, because PyPyC has a (documented) + incompatibility in the semantics of this function. + """ + t = TraversalBreadthFirstFlex( + self._vertex_to_id, self._gear, lambda v, t: [], is_tree=self._is_tree + ) + # Attributes of Traversal + # (except for _is_tree and _vertex_to_id that we have already set) + t._labeled_edges = self._labeled_edges + t._generator = self._generator + t._start_vertices = self._start_vertices + t._build_paths = self._build_paths + t._calculation_limit = self._calculation_limit + t.paths = self.paths + t._predecessors = self._predecessors + t._attributes = self._attributes + # Attributes of _TraversalWithoutWeights + # (except for _gear that we have already set) + t._edges_with_data = self._edges_with_data + # Attributes of _TraversalWithoutWeightsWIthVisited + t.visited = self.visited + # Attributes of TraversalBreadthFirstFlex + t.depth = self.depth + t._report_depth_increase = self._report_depth_increase + return t + def start_from( self, start_vertex: Optional[T_vertex] = None, @@ -137,7 +169,7 @@ def start_from( calculation_limit: Optional[int] = None, already_visited: Optional[VertexIdSet[T_vertex_id]] = None, _report_depth_increase: bool = False, # hidden parameter for internal use - ) -> TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. @@ -240,7 +272,7 @@ def _traverse(self) -> Generator[T_vertex, None, Any]: # one, while for the latter, it needs to be one higher. In order to avoid # a cascade of +1 and -1 on the depth, we just use a copy of the traversal, # that hangs by one in the depth, and give this to next_edge_or_vertices. - prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + prev_traversal = self._copy() # copy of self, for keeping previous depth self.depth = 1 # used for reporting (prev_traversal starts at 0) # Get method references of specific bookkeeping (avoid attribute resolution) @@ -413,6 +445,8 @@ class TraversalBreadthFirst( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = TraversalBreadthFirstFlex._state_attrs + def __init__( self, next_vertices: Optional[ diff --git a/src/nographs/_strategies/traversals/without_weights/depth_first.py b/src/nographs/_strategies/traversals/without_weights/depth_first.py index b33b3d8..649ebb5 100644 --- a/src/nographs/_strategies/traversals/without_weights/depth_first.py +++ b/src/nographs/_strategies/traversals/without_weights/depth_first.py @@ -1,9 +1,8 @@ -from __future__ import annotations - import array -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Generator -from enum import Flag, auto + +# from enum import Flag, auto from nographs._types import ( T_vertex, @@ -39,132 +38,7 @@ _TraversalWithoutWeightsWithVisited, ) - -class DFSEvent(Flag): - """ - An enumeration of the events that can trigger the report of a vertex / edge by - TraversalDepthFirst. - - Events reporting that a vertex is entered or left: - - - ENTERING_START: A start vertex has been entered and the traversal starts - there. - - - LEAVING_START: A start vertex has been left (the traversal may continue - with the next one). - - - ENTERING_SUCCESSOR: A vertex is entered, when an edge - that leads to it is followed. In mode *DFS_TREE*, only *DFS-tree edges* - are followed. - - - LEAVING_SUCCESSOR: A vertex is left, when an edge that leads to it - and has been followed, is now followed in reversed direction, during - backtracking from the edge. In mode *DFS_TREE*, only *DFS-tree edges* - are followed. - - Events reporting that a vertex (or an edge) has been detected but will not be - entered (resp. followed): - - - SKIPPING_START: A start vertex was about to be entered, as start of a - traversal from there, but it has already been visited as descendant of - another start vertex, and thus, it is skipped. - - - BACK_EDGE: An edge *(u, v)* is found, where v has already been entered, - but not left so far. In other words, *v* is on the trace (path that - leads to *u* within the tree). - - - FORWARD_EDGE: An edge *(u, v)* is found, where *v* has already been - left, and it had been entered after *u*. *(u, v)* is a shortcut - forwards in the tree branch from *u* to *v*, so to speak. - - - CROSS_EDGE: An edge *(u, v)* is found, where *v* has already been left, - and it had been entered before *u*. This means, in the DFS tree, - *u* and *v* do not have any ancestor or descendant relationship - between them. - - Events that combine other events as a group (*group-events*): - - - SOME_NON_TREE_EDGE: One of the events FORWARD_EDGE, BACK_EDGE, or CROSS_EDGE - occurred, but it has not been determined which of these events. - - - FORWARD_OR_CROSS_EDGE: One of the events FORWARD_EDGE or CROSS_EDGE - occurred, but it has not been determined which of these events. - - - Aliases for sets of events: - - - NONE = 0 - - - ENTERING = ENTERING_START | ENTERING_SUCCESSOR - - LEAVING = LEAVING_START | LEAVING_SUCCESSOR - - - IN_OUT_START = ENTERING_START | LEAVING_START - - IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR - - IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR - - - NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE - - EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES - - - ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES - """ - - ENTERING_START = auto() - LEAVING_START = auto() - - ENTERING_SUCCESSOR = auto() - LEAVING_SUCCESSOR = auto() - - SKIPPING_START = auto() - BACK_EDGE = auto() - FORWARD_EDGE = auto() - CROSS_EDGE = auto() - - SOME_NON_TREE_EDGE = auto() - FORWARD_OR_CROSS_EDGE = auto() - - NONE = 0 - - ENTERING = ENTERING_START | ENTERING_SUCCESSOR - LEAVING = LEAVING_START | LEAVING_SUCCESSOR - - IN_OUT_START = ENTERING_START | LEAVING_START - IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR - IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR - - NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE - EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES - - ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES - - -class DFSMode(Flag): - """ - An enumeration of the traversing mode to be used by TraversalDepthFirst. - - The modes are: - - - DFS_TREE: The traversal follows the edges of the DFS tree. If demanded, - non-tree edges are reported, but not followed. Vertices are only - visited once. - - - ALL_PATHS: A simple path is a path that does not contain a vertex twice. - In this mode, the traversal follows all edges, also edges leading to - vertices that have already been visited. But edges to vertices, that are - already on the trace (current path from a start vertex to the current - vertex) are ignored. For example, this can be used to search in the set - of all possible simple paths from some edges to some others. - - - ALL_WALKS: A walk is a sequence of nodes in which each adjacent pair of - nodes in the sequence is adjacent in the graph. - A walk can contain the same vertex or edge more than once. - In this more, the traversal follows all edges, also edges leading to - vertices that have already been followed as part of the trace (the - current walk from a start vertex to the current vertex). - """ - - DFS_TREE = auto() - ALL_PATHS = auto() - ALL_WALKS = auto() +from .depth_first_enum_types import DFSEvent, DFSMode class TraversalDepthFirstFlex( @@ -217,25 +91,34 @@ class TraversalDepthFirstFlex( *on_trace*, and *index*. """ + _state_attrs: ClassVar = _TraversalWithoutWeightsWithVisited._state_attrs + [ + "depth", + "event", + "trace", + "trace_labels", + "on_trace", + "index", + ] + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], next_vertices: Optional[ NextVertices[ - T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels] + T_vertex, "TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]" ] ] = None, *, next_edges: Optional[ NextEdges[ - T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels] + T_vertex, "TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]" ] ] = None, next_labeled_edges: Optional[ NextLabeledEdges[ T_vertex, - TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels], + "TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]", T_labels, ] ] = None, @@ -305,7 +188,7 @@ def start_from( compute_trace: bool = False, compute_on_trace: bool = False, compute_index: bool = False, - ) -> TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. @@ -452,7 +335,7 @@ def start_from( DFSEvent.NON_TREE_EDGES | DFSEvent.SOME_NON_TREE_EDGE | DFSEvent.FORWARD_OR_CROSS_EDGE - ) and (self._is_tree or mode in DFSMode.ALL_WALKS): + ) and (self._is_tree or mode == DFSMode.ALL_WALKS): raise RuntimeError( "The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, " "and groups containing them, " @@ -575,6 +458,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # Create individual flags for events that are to be reported # (Avoids attribute resolution both for checking if an event has # to be reported and for the value that is then to report) + # MyPy: Flag cannot be compiled, so it is excluded from compilation. event_entering = DFSEvent.ENTERING_SUCCESSOR event_entering_start = DFSEvent.ENTERING_START report_leaving = DFSEvent.LEAVING_SUCCESSOR & report @@ -588,9 +472,8 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: report_forward_or_cross_edge = DFSEvent.FORWARD_OR_CROSS_EDGE & report report_none = DFSEvent.NONE # The same for the mode - mode_dfs_tree = mode & DFSMode.DFS_TREE - # mode_simple_paths = mode & DFSMode.ALL_PATHS - mode_walks = mode & DFSMode.ALL_WALKS + mode_dfs_tree = mode == DFSMode.DFS_TREE + mode_walks = mode == DFSMode.ALL_WALKS # ----- Initialize method specific bookkeeping ----- @@ -636,15 +519,21 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: and visited_uses_bits == on_trace_uses_bits ), ("Collection visited is incompatible " + "with collection on_trace") set_uses_sequence = visited_uses_sequence - del visited_uses_sequence, on_trace_uses_sequence + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del visited_uses_sequence, on_trace_uses_sequence set_uses_bits = visited_uses_bits - del visited_uses_bits, on_trace_uses_bits + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del visited_uses_bits, on_trace_uses_bits if set_uses_sequence and set_uses_bits: assert visited_index_and_bit_method is on_trace_index_and_bit_method, ( "Collection visited is incompatible " + "with collection on_trace" ) set_index_and_bit_method = visited_index_and_bit_method - del visited_index_and_bit_method, on_trace_index_and_bit_method + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del visited_index_and_bit_method, on_trace_index_and_bit_method # Start times of vertices (the collection implicitly default to 0) _, index_sequence, index_wrapper = access_to_vertex_mapping(index) @@ -764,7 +653,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # already visited and need to check and handle this; and we have to # update the visited set. # In mode ALL_WALKS, we ignore all this - if not is_tree and mode != mode_walks: + if not is_tree and not mode_walks: event = report_none @@ -775,7 +664,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # of another vertex, or because it is given as start vertex twice), # and in mode ALL_PATHS that it already is on_trace. re_visit = False - if mode == mode_dfs_tree: + if mode_dfs_tree: if not set_uses_sequence: # Standard implementation for "normal" MutableSet if v_id in visited: @@ -932,7 +821,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: continue # The vertex has not been visited before, and we are still - # in case "not is_tree and mode != mode_walks" + # in case "not is_tree and not mode_walks" if compute_on_trace: # Add to trace set, if not already done @@ -1039,7 +928,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # Proof for correctness of the type hole: # self._labeled_edges -> next_edges (a NextWeightedEdges) is # a NextWeightedLabeledEdges -> {edge_data_expr} is a T_labels - labels = edge_or_vertex[-1] # type: ignore[assignment] + labels = edge_or_vertex[-1] to_visit_labels_append(labels) # Store marker False: when it is reached later on, we know @@ -1470,6 +1359,8 @@ class TraversalDepthFirst( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = TraversalDepthFirstFlex._state_attrs + def __init__( self, next_vertices: Optional[ diff --git a/src/nographs/_strategies/traversals/without_weights/depth_first_enum_types.py b/src/nographs/_strategies/traversals/without_weights/depth_first_enum_types.py new file mode 100644 index 0000000..ddb69c2 --- /dev/null +++ b/src/nographs/_strategies/traversals/without_weights/depth_first_enum_types.py @@ -0,0 +1,128 @@ +from enum import Flag, Enum, auto + + +class DFSEvent(Flag): + """ + An enumeration of the events that can trigger the report of a vertex / edge by + TraversalDepthFirst. + + Events reporting that a vertex is entered or left: + + - ENTERING_START: A start vertex has been entered and the traversal starts + there. + + - LEAVING_START: A start vertex has been left (the traversal may continue + with the next one). + + - ENTERING_SUCCESSOR: A vertex is entered, when an edge + that leads to it is followed. In mode *DFS_TREE*, only *DFS-tree edges* + are followed. + + - LEAVING_SUCCESSOR: A vertex is left, when an edge that leads to it + and has been followed, is now followed in reversed direction, during + backtracking from the edge. In mode *DFS_TREE*, only *DFS-tree edges* + are followed. + + Events reporting that a vertex (or an edge) has been detected but will not be + entered (resp. followed): + + - SKIPPING_START: A start vertex was about to be entered, as start of a + traversal from there, but it has already been visited as descendant of + another start vertex, and thus, it is skipped. + + - BACK_EDGE: An edge *(u, v)* is found, where v has already been entered, + but not left so far. In other words, *v* is on the trace (path that + leads to *u* within the tree). + + - FORWARD_EDGE: An edge *(u, v)* is found, where *v* has already been + left, and it had been entered after *u*. *(u, v)* is a shortcut + forwards in the tree branch from *u* to *v*, so to speak. + + - CROSS_EDGE: An edge *(u, v)* is found, where *v* has already been left, + and it had been entered before *u*. This means, in the DFS tree, + *u* and *v* do not have any ancestor or descendant relationship + between them. + + Events that combine other events as a group (*group-events*): + + - SOME_NON_TREE_EDGE: One of the events FORWARD_EDGE, BACK_EDGE, or CROSS_EDGE + occurred, but it has not been determined which of these events. + + - FORWARD_OR_CROSS_EDGE: One of the events FORWARD_EDGE or CROSS_EDGE + occurred, but it has not been determined which of these events. + + + Aliases for sets of events: + + - NONE = 0 + + - ENTERING = ENTERING_START | ENTERING_SUCCESSOR + - LEAVING = LEAVING_START | LEAVING_SUCCESSOR + + - IN_OUT_START = ENTERING_START | LEAVING_START + - IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR + - IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR + + - NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE + - EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES + + - ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES + """ + + ENTERING_START = auto() + LEAVING_START = auto() + + ENTERING_SUCCESSOR = auto() + LEAVING_SUCCESSOR = auto() + + SKIPPING_START = auto() + BACK_EDGE = auto() + FORWARD_EDGE = auto() + CROSS_EDGE = auto() + + SOME_NON_TREE_EDGE = auto() + FORWARD_OR_CROSS_EDGE = auto() + + NONE = 0 + + ENTERING = ENTERING_START | ENTERING_SUCCESSOR + LEAVING = LEAVING_START | LEAVING_SUCCESSOR + + IN_OUT_START = ENTERING_START | LEAVING_START + IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR + IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR + + NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE + EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES + + ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES + + +class DFSMode(Enum): + """ + An enumeration of the traversing mode to be used by TraversalDepthFirst. + + The modes are: + + - DFS_TREE: The traversal follows the edges of the DFS tree. If demanded, + non-tree edges are reported, but not followed. Vertices are only + visited once. + + - ALL_PATHS: A simple path is a path that does not contain a vertex twice. + In this mode, the traversal follows all edges, also edges leading to + vertices that have already been visited. But edges to vertices, that are + already on the trace (current path from a start vertex to the current + vertex) are ignored. For example, this can be used to search in the set + of all possible simple paths from some edges to some others. + + - ALL_WALKS: A walk is a sequence of nodes in which each adjacent pair of + nodes in the sequence is adjacent in the graph. + A walk can contain the same vertex or edge more than once. + In this more, the traversal follows all edges, also edges leading to + vertices that have already been followed as part of the trace (the + current walk from a start vertex to the current vertex). + """ + + DFS_TREE = auto() + ALL_PATHS = auto() + ALL_WALKS = auto() diff --git a/src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py b/src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py index dffdbee..48df8b7 100644 --- a/src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py +++ b/src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py @@ -1,9 +1,6 @@ -from __future__ import annotations - import array -import copy import itertools -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Generator from nographs._types import ( @@ -83,6 +80,10 @@ class TraversalNeighborsThenDepthFlex( *depth*, *paths*, and *visited*. """ + _state_attrs: ClassVar = _TraversalWithoutWeightsWithVisited._state_attrs + [ + "depth" + ] + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], @@ -90,20 +91,20 @@ def __init__( next_vertices: Optional[ NextVertices[ T_vertex, - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], + "TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]", ] ] = None, *, next_edges: Optional[ NextEdges[ T_vertex, - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], + "TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]", ] ] = None, next_labeled_edges: Optional[ NextLabeledEdges[ T_vertex, - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], + "TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]", T_labels, ] ] = None, @@ -128,6 +129,37 @@ def __init__( """ self._compute_depth = False # value not used, initialized during traversal + def _copy( + self, + ) -> "TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]": + """Return a copy of the current traversal. + + (We cannot use copy.copy() instead, because PyPyC has a (documented) + incompatibility in the semantics of this function. + """ + t = TraversalNeighborsThenDepthFlex( + self._vertex_to_id, self._gear, lambda v, t: [], is_tree=self._is_tree + ) + # Attributes of Traversal + # (except for _is_tree and _vertex_to_id that we have already set) + t._labeled_edges = self._labeled_edges + t._generator = self._generator + t._start_vertices = self._start_vertices + t._build_paths = self._build_paths + t._calculation_limit = self._calculation_limit + t.paths = self.paths + t._predecessors = self._predecessors + t._attributes = self._attributes + # Attributes of _TraversalWithoutWeights + # (except for _gear that we have already set) + t._edges_with_data = self._edges_with_data + # Attributes of _TraversalWithoutWeightsWIthVisited + t.visited = self.visited + # Attributes of TraversalNeighborsThenDepthFlex + t.depth = self.depth + t._compute_depth = self._compute_depth + return t + def start_from( self, start_vertex: Optional[T_vertex] = None, @@ -137,7 +169,7 @@ def start_from( calculation_limit: Optional[int] = None, already_visited: Optional[VertexIdSet[T_vertex_id]] = None, compute_depth: bool = False, - ) -> TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. @@ -239,7 +271,7 @@ def _traverse(self) -> Generator[T_vertex, None, Any]: depth = -1 # The inner loop starts with incrementing, so, we pre-decrement if not compute_depth: self.depth = depth # In this case, we leave the -1 the whole time - prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + prev_traversal = self._copy() # copy of self, for keeping previous depth # vertices to expand to_expand = self._gear.sequence_of_vertices(self._start_vertices) @@ -372,6 +404,8 @@ class TraversalNeighborsThenDepth( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = TraversalNeighborsThenDepthFlex._state_attrs + def __init__( self, next_vertices: Optional[ diff --git a/src/nographs/_strategies/traversals/without_weights/topological_sort.py b/src/nographs/_strategies/traversals/without_weights/topological_sort.py index 3114c73..8715454 100644 --- a/src/nographs/_strategies/traversals/without_weights/topological_sort.py +++ b/src/nographs/_strategies/traversals/without_weights/topological_sort.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import array import itertools -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Generator from nographs._types import ( @@ -88,25 +86,32 @@ class TraversalTopologicalSortFlex( *depth*, *paths*, and *visited*. """ + _state_attrs: ClassVar = _TraversalWithoutWeightsWithVisited._state_attrs + [ + "depth", + "cycle_from_start", + ] + def __init__( self, vertex_to_id: VertexToID[T_vertex, T_vertex_id], gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], next_vertices: Optional[ NextVertices[ - T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels] + T_vertex, + "TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]", ] ] = None, *, next_edges: Optional[ NextEdges[ - T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels] + T_vertex, + "TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]", ] ] = None, next_labeled_edges: Optional[ NextLabeledEdges[ T_vertex, - TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels], + "TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]", T_labels, ] ] = None, @@ -141,7 +146,7 @@ def start_from( build_paths: bool = False, calculation_limit: Optional[int] = None, already_visited: Optional[VertexIdSet[T_vertex_id]] = None, - ) -> TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. @@ -426,15 +431,21 @@ def _traverse_in_non_tree(self) -> Generator[T_vertex, None, Any]: and visited_uses_bits == trace_set_uses_bits ), ("Collection visited is incompatible " + "with collection trace_set") set_uses_sequence = visited_uses_sequence - del visited_uses_sequence, trace_set_uses_sequence + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del visited_uses_sequence, trace_set_uses_sequence set_uses_bits = visited_uses_bits - del visited_uses_bits, trace_set_uses_bits + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del visited_uses_bits, trace_set_uses_bits if set_uses_sequence and set_uses_bits: assert visited_index_and_bit_method is trace_set_index_and_bit_method, ( "Collection visited is incompatible " + "with collection trace_set" ) set_index_and_bit_method = visited_index_and_bit_method - del visited_index_and_bit_method, trace_set_index_and_bit_method + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del visited_index_and_bit_method, trace_set_index_and_bit_method # ----- Inner loop ----- @@ -674,6 +685,8 @@ class TraversalTopologicalSort( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = TraversalTopologicalSortFlex._state_attrs + def __init__( self, next_vertices: Optional[ diff --git a/src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py b/src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py index d533832..cecd14d 100644 --- a/src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py +++ b/src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py @@ -1,7 +1,5 @@ -from __future__ import annotations - from abc import ABC -from typing import Optional, Any +from typing import Optional, Any, ClassVar from _collections_abc import Iterable, Collection from nographs._types import ( @@ -71,6 +69,8 @@ class _TraversalWithoutWeights(Traversal[T_vertex, T_vertex_id, T_labels], ABC): A traversal that needs no weight type. Edges can be given with or without data. """ + _state_attrs: ClassVar = Traversal._state_attrs + def __init__( self, edges_with_data: bool, @@ -95,6 +95,8 @@ class _TraversalWithoutWeightsWithVisited( ): """A _TraversalWithoutWeights with attribute visited.""" + _state_attrs: ClassVar = _TraversalWithoutWeights._state_attrs + ["visited"] + def __init__( self, edges_with_data: bool, @@ -125,7 +127,9 @@ def _start_without_weights_with_visited( _start_vertices, _build_path, paths, _predecessors, _attributes, and visited. Empty paths for start vertices are only set if demanded (default: True). - Start vertices are only set as visited if demanded (default: True). + Start vertices are only set as visited if demanded (default: True), and + then, if they are not given as Collection, replaced by a Collection. + If not demanded, start_vertices remain untouched (not consumed). """ _start_from_needs_traversal_object(self) self._start_from( diff --git a/src/nographs/_strategies/type_aliases.py b/src/nographs/_strategies/type_aliases.py index 997cc6e..ee403a1 100644 --- a/src/nographs/_strategies/type_aliases.py +++ b/src/nographs/_strategies/type_aliases.py @@ -1,10 +1,8 @@ -""" Type aliases used for defining the signatures of methods of strategies +"""Type aliases used for defining the signatures of methods of strategies Warning: The following types are manually documented in api.rst """ -from __future__ import annotations - from typing import TypeVar, Callable, Iterable, Any, Union from nographs._types import ( @@ -20,7 +18,7 @@ from .strategy import Strategy -T_strategy = TypeVar("T_strategy", bound=Strategy) +T_strategy = TypeVar("T_strategy", bound=Strategy[Any, Any, Any]) # next vertices and next edges functions for traversals diff --git a/src/nographs/_strategies/utils.py b/src/nographs/_strategies/utils.py index 1218a79..571dd05 100644 --- a/src/nographs/_strategies/utils.py +++ b/src/nographs/_strategies/utils.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from collections.abc import ( Iterator, Iterable, @@ -47,7 +45,7 @@ def __init__(self, s: str) -> None: self.s = s @classmethod - def from_iterable(cls, i: Iterable[tuple[Any, Any]]) -> StrRepr: + def from_iterable(cls, i: Iterable[tuple[Any, Any]]) -> "StrRepr": """ Provides a string representation of an iterable of key/value tuples, that look like the output from a dict with these items. @@ -57,7 +55,7 @@ def from_iterable(cls, i: Iterable[tuple[Any, Any]]) -> StrRepr: return cls("{" + ", ".join(repr(k) + ": " + repr(v) for k, v in i) + "}") @classmethod - def from_set(cls, c: MutableSet[Any]) -> StrRepr: + def from_set(cls, c: MutableSet[Any]) -> "StrRepr": """ Provides a string representation of a *MutableSet*, that looks like the string representation of a *Set* with these items, diff --git a/src/nographs/_types.py b/src/nographs/_types.py index 332bcc8..eaa0cf7 100644 --- a/src/nographs/_types.py +++ b/src/nographs/_types.py @@ -2,58 +2,50 @@ Callable, Hashable, ) -from typing import TypeVar, Protocol, Union +from typing import TypeVar, Union, Protocol from abc import abstractmethod -""" -Basic types used in NoGraphs. -""" +"""Basic types used in NoGraphs.""" T = TypeVar("T") T_vertex = TypeVar("T_vertex") T_vertex_id = TypeVar("T_vertex_id", bound=Hashable) -T_weight = TypeVar("T_weight", bound="Weight") +T_labels = TypeVar("T_labels") # The following class is manually documented in api.rst, keep docs consistent. -class Weight(Protocol[T_weight]): +T_w = TypeVar("T_w", bound="Weight") + + +# MyPyC: The protocol cannot be generic with parameter T_weight, because +# MyPyC crashed then. +class Weight(Protocol): @abstractmethod - def __add__(self: T_weight, value: T_weight) -> T_weight: + def __add__(self: T_w, value: T_w) -> T_w: """Return self+value.""" raise NotImplementedError @abstractmethod - def __sub__(self: T_weight, value: T_weight) -> T_weight: + def __sub__(self: T_w, value: T_w) -> T_w: """Return self-value.""" raise NotImplementedError @abstractmethod - def __lt__(self: T_weight, value: T_weight) -> bool: + def __lt__(self: T_w, value: T_w) -> bool: # inherited doc string raise NotImplementedError @abstractmethod - def __le__(self: T_weight, value: T_weight) -> bool: + def __le__(self: T_w, value: T_w) -> bool: # inherited doc string raise NotImplementedError -# class IntZero(int): -# def __new__(cls) -> "IntZero": -# return super(IntZero, cls).__new__(cls, 0) -# -# -# class FloatInf(float): -# def __new__(cls) -> "FloatInf": -# return super(FloatInf, cls).__new__(cls, "inf") -# -# -# T_weight_zero_inf_or = Union[IntZero, FloatInf, T] +# MyPyC: We use another TypeVar T_w above, because MyPyC crashed otherwise +T_weight = TypeVar("T_weight", bound=Weight) -T_labels = TypeVar("T_labels") - """ Basic type aliases, part 1 """ VertexToID = Callable[[T_vertex], T_vertex_id] diff --git a/tests/test_expanded_template.py b/tests/test_expanded_template.py index e6824f2..6d4535b 100644 --- a/tests/test_expanded_template.py +++ b/tests/test_expanded_template.py @@ -11,38 +11,39 @@ def test_expansion_result(self) -> None: pp = pymacros4py.PreProcessor() templates_root = pathlib.Path("tpl") for template in templates_root.glob("**/*"): - if not template.is_file() or not template.name.startswith("!"): - continue - print("Processing template", template) + with self.subTest(str(template)): + if not template.is_file() or not template.name.startswith("!"): + continue + template_without_exclamation_mark = template.with_name( + template.name[1:] + ) + goal = template_without_exclamation_mark.relative_to(templates_root) - template_without_exclamation_mark = template.with_name(template.name[1:]) - goal = template_without_exclamation_mark.relative_to(templates_root) + expanded = pp.expand_file(template) - expanded = pp.expand_file(template) + # format expansion results of .py files with black + if template.suffix == ".py": + tmp_file_path = pymacros4py.write_to_tempfile(expanded) + pymacros4py.run_process_with_file( + ["black", tmp_file_path], tmp_file_path + ) + expanded = pymacros4py.read_file(tmp_file_path, finally_remove=True) - # format expansion results of .py files with black - if template.suffix == ".py": - tmp_file_path = pymacros4py.write_to_tempfile(expanded) - pymacros4py.run_process_with_file( - ["black", tmp_file_path], tmp_file_path - ) - expanded = pymacros4py.read_file(tmp_file_path, finally_remove=True) + if goal.exists() and not self.overwrite: + current_content = pymacros4py.read_file(goal) + + if current_content == expanded: + continue - if goal.exists() and not self.overwrite: - current_content = pymacros4py.read_file(goal) - try: - self.assertMultiLineEqual(current_content, expanded) - except AssertionError as e: d = pp.diff( current_content, expanded, str(goal), f'expanded("{str(template)}")', ) - print(d, "\n") - raise e - else: - pymacros4py.write_file(goal, expanded) + self.fail("\n" + str(d)) + else: + pymacros4py.write_file(goal, expanded) if __name__ == "__main__": @@ -57,6 +58,7 @@ def test_expansion_result(self) -> None: parser.add_argument("overwrite", help="If 'True', overwrite") args = parser.parse_args() - c = TemplateExpansionTest() + c = TemplateExpansionTest("test_expansion_result") c.overwrite = args.overwrite == "True" - c.test_expansion_result() + + unittest.TextTestRunner(failfast=False).run(c) diff --git a/tests/test_extra_tsp.py b/tests/test_extra_tsp.py index cf9ea4f..a41bcc5 100644 --- a/tests/test_extra_tsp.py +++ b/tests/test_extra_tsp.py @@ -10,18 +10,9 @@ # noinspection PyProtectedMember from nographs._extra_tsp import ( # NOQA F401 (import needed by doc tests) _traveling_salesman_int_vertices, - GettableProto, ) -class GettableProtoTest: - """ - >>> GettableProto.__getitem__(None, None) - Traceback (most recent call last): - NotImplementedError - """ - - class TspTestsGeneral: """ Unit tests for TSP @@ -240,7 +231,9 @@ def read_tsp_problem(s: str) -> tuple[dict[str, str], dict[int, dict[int, int]]] def solve( test_name: str, - function: Callable[[], Union[tuple[int, Iterable], tuple[float, Iterable]]], + function: Callable[ + [], Union[tuple[int, Iterable[T_vertex]], tuple[float, Iterable[T_vertex]]] + ], correct_length: T_weight, graph: Mapping[T_vertex, Mapping[T_vertex, T_weight]], time_stats: bool = False, diff --git a/tests/test_gear_collections.py b/tests/test_gear_collections.py index 7dad1f9..179dd36 100644 --- a/tests/test_gear_collections.py +++ b/tests/test_gear_collections.py @@ -4,114 +4,6 @@ import nographs._gear_collections as gear_collections # noqa: F401 (used by doctests) -class ProtocolAndABCNotImplementedErrors: - """-- Abstract methods of protocols and ABCs. - - If the application calls them and ignores that they are abstract, an assertion - is to be raised to inform the application about its mistake. - Check, if this mechanism is correctly implemented. - - Note: The following calls are all illegal w.r.t. typing (only the number of - parameters is correct): Instance methods are called like a classmethod would, - the given argument for parameter self has the wrong type, and other arguments may - be illegal, too, and the generic parameters are missing. But all this does not - matter here, since the methods are to raise NotImplementedError directly and in - all cases. - - >>> nog.GettableSettableForGearProto.__getitem__(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.GettableSettableForGearProto.__setitem__(None, None, None) - Traceback (most recent call last): - NotImplementedError - - - >>> nog.SequenceForGearProto.__len__(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.SequenceForGearProto.append(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.SequenceForGearProto.extend(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.SequenceForGearProto.__iter__(None) - Traceback (most recent call last): - NotImplementedError - - - >>> nog.VertexSequenceWrapperForSetProto.sequence(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForSetProto.default(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForSetProto.extend_and_set(None, None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForSetProto.update_from_keys(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForSetProto.index_and_bit_method(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForSetProto._from_iterable(None, None) - Traceback (most recent call last): - NotImplementedError - - - >>> nog.VertexSequenceWrapperForMappingProto.sequence(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForMappingProto.default(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForMappingProto.extend_and_set(None, None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForMappingProto.update_from_keys_values(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSequenceWrapperForMappingProto.update_default(None, None) - Traceback (most recent call last): - NotImplementedError - - - >>> nog.VertexSetWrappingSequence.__iter__(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSetWrappingSequence.__len__(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSetWrappingSequence.update_from_keys(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSetWrappingSequence._from_iterable(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.VertexSetWrappingSequence.index_and_bit_method(None) - Traceback (most recent call last): - NotImplementedError - """ - - class CallToPrivateDummyFunctionality: """ The following classes are library private. Only NoGraphs is expected the @@ -119,55 +11,55 @@ class CallToPrivateDummyFunctionality: if a call ever happens, this would be unexpected and probly due to an error in NoGraphs. An assertion error is to be raised to signal this. - >>> _GettableSettableForGearAssertNoCall = ( + >>> gettable_settable_for_gear_assert_no_call = ( ... # noinspection PyProtectedMember - ... gear_collections._GettableSettableForGearAssertNoCall + ... gear_collections._GettableSettableForGearAssertNoCall[int, int, int]() ... ) - >>> _GettableSettableForGearAssertNoCall.__getitem__(None, None) + >>> gettable_settable_for_gear_assert_no_call.__getitem__(0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen - >>> _GettableSettableForGearAssertNoCall.__setitem__(None, None, None) + >>> gettable_settable_for_gear_assert_no_call.__setitem__(0, 0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen >>> # noinspection PyProtectedMember - >>> _VertexSequenceWrapperAssertNoCall = ( + >>> _vertex_sequence_wrapper_assert_no_call = ( ... # noinspection PyProtectedMember - ... gear_collections._VertexSequenceWrapperAssertNoCall + ... gear_collections._VertexSequenceWrapperAssertNoCall[int, int, int]() ... ) - >>> _VertexSequenceWrapperAssertNoCall.sequence(None) + >>> _vertex_sequence_wrapper_assert_no_call.sequence() Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen - >>> _VertexSequenceWrapperAssertNoCall.default(None) + >>> _vertex_sequence_wrapper_assert_no_call.default() Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen - >>> _VertexSequenceWrapperAssertNoCall.extend_and_set(None, None, None) + >>> _vertex_sequence_wrapper_assert_no_call.extend_and_set(0, 0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen - >>> _VertexSequenceWrapperAssertNoCall.update_from_keys(None, None) + >>> _vertex_sequence_wrapper_assert_no_call.update_from_keys(0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen - >>> _VertexSequenceWrapperAssertNoCall.index_and_bit_method(None) + >>> _vertex_sequence_wrapper_assert_no_call.index_and_bit_method() Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen - >>> _VertexSequenceWrapperAssertNoCall.update_from_keys_values(None, None) + >>> _vertex_sequence_wrapper_assert_no_call.update_from_keys_values(0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen - >>> _VertexSequenceWrapperAssertNoCall.update_default(None, None) + >>> _vertex_sequence_wrapper_assert_no_call.update_default(0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen >>> # noinspection PyProtectedMember - >>> _VertexSequenceWrapperAssertNoCall._from_iterable(None, None) + >>> _vertex_sequence_wrapper_assert_no_call._from_iterable(0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen """ @@ -184,7 +76,7 @@ class GearCollectionFunctionalityMainlyOnlyForAppCode: >>> list_factory = lambda: list() >>> ws = nog.VertexSetWrappingSequenceNoBitPacking( - ... list_factory, 1024, [1, 3]) + ... list_factory, 1024, False, [1, 3]) >>> ws {1, 3} >>> len(ws.sequence()) @@ -212,12 +104,21 @@ class GearCollectionFunctionalityMainlyOnlyForAppCode: >>> len(ws.sequence()) 2051 >>> ws.discard(2051) # Case "IndexError" - >>> ws | ({4}) # Calls _from_iterable(iterable) to create new set + >>> ws2 = nog.VertexSetWrappingSequenceNoBitPacking( + ... list_factory, 1024, False, [4]) + >>> ws.__or__(ws2) # Calls _from_iterable(iterable) to create new set {1, 2, 4, 1026} + About the previous test: + MyPyC: "ws | ws2" raises: + TypeError: unsupported operand type(s) for |: + 'VertexSetWrappingSequenceBitPacking' and + 'VertexSetWrappingSequenceBitPacking' + I expect this to work. Maybe an error in typeshed? + >>> list_factory = lambda: list() >>> ws = nog.VertexSetWrappingSequenceBitPacking( - ... list_factory, 128, [1, 3]) + ... list_factory, 128, False, [1, 3]) >>> ws {1, 3} >>> len(ws.sequence()) @@ -245,12 +146,21 @@ class GearCollectionFunctionalityMainlyOnlyForAppCode: >>> len(ws.sequence()) 258 >>> ws.discard(258*8) # Case "IndexError" - >>> ws | ({4}) # Calls _from_iterable(iterable) to create new set + >>> ws2 = nog.VertexSetWrappingSequenceBitPacking( + ... list_factory, 128, False, [4]) + >>> ws.__or__(ws2) # Calls _from_iterable(iterable) to create new set {1, 2, 4, 1032} + About the previous test: + MyPyC: "ws | ws2" raises: + TypeError: unsupported operand type(s) for |: + 'VertexSetWrappingSequenceBitPacking' and + 'VertexSetWrappingSequenceBitPacking' + I expect this to work. Maybe an error in typeshed? + >>> list_factory = lambda: list[float]() >>> ws = nog.VertexMappingWrappingSequence( - ... list_factory, float("infinity"), 1024, [(0, 0), (2, 2)]) + ... list_factory, float("infinity"), 1024, False, [(0, 0), (2, 2)]) >>> ws.default() # Gap marker / default value of the mapping emulation inf >>> ws.sequence()[:5] # Given values are set, others are gap-marker / default @@ -325,7 +235,7 @@ class GearCollectionTestsForDoNotCallCases: >>> c = set() >>> res = gear_collections.access_to_vertex_set(c) >>> is_wrapper, gettable_settable, wrapper, uses_bits, index_and_bit_method = res - >>> index_and_bit_method(None, None) + >>> index_and_bit_method(0, 0) Traceback (most recent call last): AssertionError: Call to a method of this object is not expected to ever happen """ diff --git a/tests/test_gears.py b/tests/test_gears.py index 7235ced..253f6b9 100644 --- a/tests/test_gears.py +++ b/tests/test_gears.py @@ -1,60 +1,6 @@ import nographs as nog # noqa: F401 (used in doctests, undetected by flake 8) -class ProtocolAndABCNotImplementedErrors: - """ - Abstract methods of protocols and ABCs. - - If the application calls them and ignores that they are abstract, an assertion - is to be raised to inform the application about its mistake. - Check, if this mechanism is correctly implemented. - - Note: The following calls are all illegal w.r.t. typing (only the number of - parameters is correct): Instance methods are called like a classmethod would, - the given argument for parameter self has the wrong type, and other arguments may - be illegal, too, and the generic parameters are missing. But all this does not - matter here, since the methods are to raise NotImplementedError directly and in - all cases. - - - >>> nog.GearWithoutDistances.vertex_id_set(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.GearWithoutDistances.vertex_id_to_vertex_mapping(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.GearWithoutDistances.vertex_id_to_edge_labels_mapping(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.GearWithoutDistances.sequence_of_vertices(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.GearWithoutDistances.sequence_of_edge_labels(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.GearWithoutDistances.vertex_id_to_number_mapping(None, None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.Gear.zero(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.Gear.infinity(None) - Traceback (most recent call last): - NotImplementedError - - >>> nog.Gear.vertex_id_to_distance_mapping(None, None) - Traceback (most recent call last): - NotImplementedError - """ - - class SpecialFeatures: """Tests for features that are not covered by the behaviour tests >>> g = nog.GearForHashableVertexIDs[int, int, int, int](0.0, float("infinity")) diff --git a/tests/test_nographs.py b/tests/test_nographs.py index d51c08a..b6c79db 100644 --- a/tests/test_nographs.py +++ b/tests/test_nographs.py @@ -2,15 +2,23 @@ """Test functionality and documentation of NoGraphs. With parameter *fast*, slow tests are skipped. """ + + # First, we ensure that we test the right thing by showing sys.path + import sys + + print(">> sys.path used for the tests:") + for p in sys.path: + print(p) + print() + import doctest import unittest - import coverage # type: ignore + import coverage import importlib import pathlib from utils import DocTestFinderSkippingSlowTests, DocTestParserSkippingSlowTests - import sys - # detect if we have package pymacros4py + # Detect if we have package pymacros4py skip_macro_consistency_check = False try: import pymacros4py # noqa: F401 @@ -24,10 +32,15 @@ # test coverage. During CI, the flag needs to be set to FALSE in order to # fully test everything, e.g., also long-running examples of the tutorial.) skip_slow_tests = False + compute_coverage = True print(">>", sys.argv) - if len(sys.argv) > 1 and sys.argv[1] == "fast": - skip_slow_tests = True - print("Executing only fast tests") + for arg in sys.argv[1:]: + if arg == "fast": + skip_slow_tests = True + print("Executing only fast tests!") + elif arg == "no_coverage": + compute_coverage = False + print("Coverage not computed!") test_finder = ( DocTestFinderSkippingSlowTests() if skip_slow_tests else doctest.DocTestFinder() ) @@ -36,8 +49,9 @@ ) # Start recording coverage - cov = coverage.Coverage(source_pkgs=["nographs"]) - cov.start() + if compute_coverage: + cov = coverage.Coverage(source_pkgs=["nographs"]) + cov.start() # Create empty TestSuite test_suite = unittest.TestSuite() @@ -80,11 +94,13 @@ ) verbosity = 1 # 1 normal, 2 for more details - failfast = True # True + failfast = False # True unittest.TextTestRunner(verbosity=verbosity, failfast=failfast).run(test_suite) # Stop recording coverage, create HTML from results - cov.stop() - cov.save() - cov.xml_report() - cov.html_report() + if compute_coverage: + cov.stop() + cov.save() + cov.xml_report() + cov.html_report() + sys.exit() diff --git a/tests/test_paths.py b/tests/test_paths.py index 7dc88d1..7d70a8f 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -107,10 +107,10 @@ class PathsHandling: >>> gear = nog.GearForIntVertexIDsAndCFloats() >>> paths = _paths.PathsOfUnlabeledEdges( ... gear.vertex_id_to_vertex_mapping(()), - ... None + ... nog.vertex_as_id ... ) - >>> paths.append_edge(0, 0, [0]) - >>> paths.append_edge(0, 1, [1]) + >>> paths.append_edge(0, 0, None) + >>> paths.append_edge(0, 1, None) >>> paths[1] (0, 1) @@ -119,10 +119,10 @@ class PathsHandling: >>> paths = _paths.PathsOfLabeledEdges( ... gear.vertex_id_to_vertex_mapping(()), ... gear.vertex_id_to_edge_labels_mapping(()), - ... None + ... nog.vertex_as_id ... ) - >>> paths.append_edge(0, 0, [0]) - >>> paths.append_edge(0, 1, [1]) + >>> paths.append_edge(0, 0, (0, 1)) + >>> paths.append_edge(0, 1, (0, 1)) >>> paths[1] ((0, 1, 1),) diff --git a/tests/test_strategies.py b/tests/test_strategies.py index 84c7a01..6422fae 100644 --- a/tests/test_strategies.py +++ b/tests/test_strategies.py @@ -15,50 +15,52 @@ class MethodsOfDummyCollectionClasses: >>> # noinspection PyProtectedMember >>> from nographs._strategies.utils import NoVisitedSet - >>> NoVisitedSet.__contains__(None, None) + >>> s = NoVisitedSet() + >>> s.__contains__(None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoVisitedSet.__iter__(None) + >>> s.__iter__() Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoVisitedSet.__len__(None) + >>> s.__len__() Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoVisitedSet.discard(None, None) + >>> s.discard(None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoVisitedSet.add(None, None) + >>> s.add(None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed >>> # noinspection PyProtectedMember >>> from nographs._strategies.utils import NoDistancesMapping - >>> NoDistancesMapping.__getitem__(None, None) + >>> m = NoDistancesMapping() + >>> m.__getitem__(None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoDistancesMapping.__delitem__(None, None) + >>> m.__delitem__(None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoDistancesMapping.__iter__(None) + >>> m.__iter__() Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoDistancesMapping.__len__(None) + >>> m.__len__() Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoDistancesMapping.__contains__(None, None) + >>> m.__contains__(None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed - >>> NoDistancesMapping.__setitem__(None, None, None) + >>> m.__setitem__(None, None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed """ diff --git a/tests/test_traversals_and_searches.py b/tests/test_traversals_and_searches.py index 4470857..eaba10e 100644 --- a/tests/test_traversals_and_searches.py +++ b/tests/test_traversals_and_searches.py @@ -68,7 +68,8 @@ def _results_of_traversal( of __dict__ of traversal object so that the caller can perform further checks. """ # get traversal attributes before iterator starts, for comparison - org_dict = dict(traversal.__dict__) + # noinspection PyProtectedMember + org_dict = {k: getattr(traversal, k) for k in traversal._state_attrs} vertices = set(additional_vertices) print_filled(f"After start: {traversal.state_to_str(vertices)}") for vertex in traversal: @@ -169,7 +170,9 @@ def print_partial_results( def check_path( vertex_iterable: Iterable[T_vertex], - next_edges: Callable[[T_vertex, Any], Iterable[tuple]], + next_edges: Callable[ + [T_vertex, Any], Iterable[nog.AnyFullEdge[T_vertex, Any, Any]] + ], ) -> None: """Check if each edge in the path described by the vertex_iterable is allowed according to the given next_edges function. @@ -184,12 +187,13 @@ def check_path( def adj_funcs_bi_from_list( - edge_list: Iterable, + edge_list: Iterable[nog.AnyFullEdge[T_vertex, Any, Any]], edge_data: bool, report_steps: bool = True, - vertex_to_key: Callable = nog.vertex_as_id, + vertex_to_key: Callable[[Any], Any] = nog.vertex_as_id, ) -> tuple[ - Callable[[T_vertex, Strategy], Iterable], Callable[[T_vertex, Strategy], Iterable] + Callable[[T_vertex, Strategy[Any, Any, Any]], Iterable[Any]], + Callable[[T_vertex, Strategy[Any, Any, Any]], Iterable[Any]], ]: """Create NextEdges function for forward and for backward direction, based on an Iterable of edges. When a vertex is expanded, this is printed to std out, @@ -203,8 +207,8 @@ def adj_funcs_bi_from_list( vertex_to_key need to be a function that returns a hashable key for a vertex, where no two vertices have the same key. """ - edge_dict_forwards = collections.defaultdict(list) - edge_dict_backwards = collections.defaultdict(list) + edge_dict_forwards = collections.defaultdict[T_vertex, list[Any]](list) + edge_dict_backwards = collections.defaultdict[T_vertex, list[Any]](list) if edge_data: for edge in edge_list: v, w, *others = edge @@ -218,20 +222,28 @@ def adj_funcs_bi_from_list( if report_steps: - def next_edges_forwards(vertex: T_vertex, strategy: Strategy) -> Iterable: + def next_edges_forwards( + vertex: T_vertex, strategy: Strategy[Any, Any, Any] + ) -> Iterable[Any]: print_filled(f"? {vertex}: {strategy.state_to_str([vertex])}") return edge_dict_forwards.get(vertex_to_key(vertex), []) - def next_edges_backwards(vertex: T_vertex, strategy: Strategy) -> Iterable: + def next_edges_backwards( + vertex: T_vertex, strategy: Strategy[Any, Any, Any] + ) -> Iterable[Any]: print_filled(f"?<{vertex}: {strategy.state_to_str([vertex])}") return edge_dict_backwards.get(vertex_to_key(vertex), []) else: - def next_edges_forwards(vertex: T_vertex, strategy: Strategy) -> Iterable: + def next_edges_forwards( + vertex: T_vertex, strategy: Strategy[Any, Any, Any] + ) -> Iterable[Any]: return edge_dict_forwards.get(vertex_to_key(vertex), []) - def next_edges_backwards(vertex: T_vertex, strategy: Strategy) -> Iterable: + def next_edges_backwards( + vertex: T_vertex, strategy: Strategy[Any, Any, Any] + ) -> Iterable[Any]: return edge_dict_backwards.get(vertex_to_key(vertex), []) return next_edges_forwards, next_edges_backwards @@ -250,10 +262,14 @@ def first_of(lst: list[T]) -> T: infinity = float("infinity") V = TypeVar("V") -E = TypeVar("E", bound=tuple) +W = TypeVar("W", bound=nog.Weight) +L = TypeVar("L") +E = nog.AnyFullEdge[V, W, L] +# E = TypeVar("E", bound=Sequence[Any]) +# E = nog.AnyFullEdge[Any, Any, Any] -class Fixture(ABC, Generic[V, E]): +class Fixture(ABC, Generic[V, W, L]): """Basic test fixture. Provides adjacency function next_vertices and a start vertex. """ @@ -261,10 +277,10 @@ class Fixture(ABC, Generic[V, E]): def __init__(self, start: V): self.start = start - next_edges: Callable[[V, Strategy], Iterable[E]] + next_edges: Callable[[V, Strategy[Any, Any, Any]], Iterable[E[V, W, L]]] -class FixtureFull(Fixture[V, E]): +class FixtureFull(Fixture[V, W, L]): """Test fixture. Provides adjacency functions next_vertices, next_vertices_bi, next_edges, and next_edges_bi for some given edges. Optionally, these functions report their state. Optionally, a vertex_to_key @@ -275,12 +291,12 @@ class FixtureFull(Fixture[V, E]): def __init__( self, - edges: Iterable[E], + edges: Iterable[E[V, W, L]], start: V, goal: V, heuristic: Callable[[V], Union[int, float]], report: bool = False, - vertex_to_key: Callable = nog.vertex_as_id, + vertex_to_key: Callable[[Any], Any] = nog.vertex_as_id, ): super().__init__(start) self.edges = edges @@ -289,60 +305,66 @@ def __init__( self.start_bi = (start, goal) - self.next_vertices_bi = adj_funcs_bi_from_list( + self.next_vertices_bi: tuple[ + Callable[[T_vertex, Strategy[Any, Any, Any]], Iterable[E[V, W, L]]], + Callable[[T_vertex, Strategy[Any, Any, Any]], Iterable[E[V, W, L]]], + ] = adj_funcs_bi_from_list( edges, edge_data=False, report_steps=report, vertex_to_key=vertex_to_key ) self.next_vertices = self.next_vertices_bi[0] - self.next_edges_bi = adj_funcs_bi_from_list( + self.next_edges_bi: tuple[ + Callable[[T_vertex, Strategy[Any, Any, Any]], Iterable[E[V, W, L]]], + Callable[[T_vertex, Strategy[Any, Any, Any]], Iterable[E[V, W, L]]], + ] = adj_funcs_bi_from_list( edges, edge_data=True, report_steps=report, vertex_to_key=vertex_to_key ) self.next_edges = self.next_edges_bi[0] -class FNoEdgesGoalUnreachable(FixtureFull[int, tuple]): +class FNoEdgesGoalUnreachable(FixtureFull[int, Any, Any]): """Zero edges graph, goal not reachable""" def __init__(self) -> None: super().__init__([], 0, 1, lambda v: 0, report=True) -class FNoEdgesGoalIsStart(FixtureFull[int, tuple]): +class FNoEdgesGoalIsStart(FixtureFull[int, Any, Any]): """Zero edges graph, start equals goal""" def __init__(self) -> None: super().__init__([], 0, 0, lambda v: 0, report=True) -class FOneEdgeNoData(FixtureFull[int, tuple[int, int]]): +class FOneEdgeNoData(FixtureFull[int, Any, Any]): """One-edge graph with edge from start to goal, no weights, no labels""" def __init__(self) -> None: super().__init__([(0, 1)], 0, 1, lambda v: 1 if v == 0 else 0) -class FOneEdgeWeighted(FixtureFull[int, tuple[int, int, int]]): +class FOneEdgeWeighted(FixtureFull[int, int, int]): """One-edge graph with edge from start to goal, weight 1, no labels""" def __init__(self) -> None: super().__init__([(0, 1, 1)], 0, 1, lambda v: 1 if v == 0 else 0) -class FOneEdgeLabeled(FixtureFull[int, tuple[int, int, int]]): +class FOneEdgeLabeled(FixtureFull[int, int, int]): """One-edge graph with edge from start to goal, no weights, label 2""" def __init__(self) -> None: super().__init__([(0, 1, 2)], 0, 1, lambda v: 1 if v == 0 else 0) -class FOneEdgeWeightedLabeled(FixtureFull[int, tuple[int, int, int, int]]): +class FOneEdgeWeightedLabeled(FixtureFull[int, int, int]): """One-edge graph with edge from start to goal, weight 1, label 2""" def __init__(self) -> None: super().__init__([(0, 1, 1, 2)], 0, 1, lambda v: 1 if v == 0 else 0) -class FOneEdgeUnhashable(FixtureFull[list[int], tuple[list[int], list[int], int, int]]): +class FOneEdgeUnhashable(FixtureFull[list[int], int, int]): """One-edge graph - vertices are list[int]""" def __init__(self) -> None: @@ -355,9 +377,7 @@ def __init__(self) -> None: ) -class FSequenceUnhashable( - FixtureFull[list[int], tuple[list[int], list[int], int, int]] -): +class FSequenceUnhashable(FixtureFull[list[int], int, int]): """Linear graph of fixed size - vertices are list[int]""" def __init__(self) -> None: @@ -374,7 +394,7 @@ def __init__(self) -> None: self.start_impossible_bi = (self.start, self.goal_impossible) -class FDiamond(FixtureFull[int, tuple[int, int, int, int]]): +class FDiamond(FixtureFull[int, int, int]): """Diamond-shaped graph of fixed size. Variant with weight 2 from vertices 1 and 2 to 3 (useful for TraversalShortestPaths). Additionally, a vertex for a test with this vertex as already visited vertex is given, and values for know distances for @@ -393,7 +413,7 @@ def __init__(self) -> None: self.values_for_known_distances = ((0, 2), (1, 0)) -class FDiamondSorted(FixtureFull[int, tuple[int, int, int]]): +class FDiamondSorted(FixtureFull[int, int, int]): """Diamond-shaped graph of fixed size. Variant with sorted edges (as needed by TraversalShortestPathsInfBranchingSorted). No heuristic is given, since it is not used for this strategy. @@ -405,7 +425,7 @@ def __init__(self) -> None: ) -class FDiamondDFS(FixtureFull[int, tuple[int, int, int, int]]): +class FDiamondDFS(FixtureFull[int, int, int]): """Diamond-shaped graph of fixed size. Variant with two additional edges of kind forward and back edge. Additionally, a vertex for a test with this vertex as already visited vertex is given. @@ -429,7 +449,7 @@ def __init__(self) -> None: self.vertex_for_already_visited = 1 -class FDiamondMST(FixtureFull[int, tuple[int, int, int]]): +class FDiamondMST(FixtureFull[int, int, int]): """Diamond-shaped graph of fixed size. Variant with weight 3 from vertices 1 and 2 to 3 (used for MST). No heuristic is given, since it is not used for A*. """ @@ -440,7 +460,7 @@ def __init__(self) -> None: ) -class FAStar(FixtureFull[int, tuple[int, int, int]]): +class FAStar(FixtureFull[int, int, int]): """A* test graph.""" def __init__(self) -> None: @@ -454,7 +474,7 @@ def __init__(self) -> None: self.values_for_known_distances = ((0, 2), (1, 0)) -class FBSearchShortestPath(FixtureFull[int, tuple[int, int, int]]): +class FBSearchShortestPath(FixtureFull[int, int, int]): """Additional test graph for BSearchShortestPath""" def __init__(self) -> None: @@ -467,7 +487,7 @@ def __init__(self) -> None: ) -class FSmallBinaryTree(FixtureFull[int, tuple[int, int, int]]): +class FSmallBinaryTree(FixtureFull[int, int, int]): """Graphs forming a binary tree with just 6 vertices. Outgoing edges are sorted by ascending weight.""" @@ -481,7 +501,7 @@ def __init__(self) -> None: ) -class FMultiStart(FixtureFull[int, tuple[int, int, int]]): +class FMultiStart(FixtureFull[int, int, int]): """Graph for testing multiple start vertices. Used for all strategies except of DFS, A* and the bidirectional search strategies. Outgoing edges are sorted by ascending weight, since all weights are equal.""" @@ -506,7 +526,7 @@ def __init__(self) -> None: self.goal_vertices = (4,) -class FMultiStartDFS(FixtureFull[int, tuple[int, int, int]]): +class FMultiStartDFS(FixtureFull[int, int, int]): """Graph for testing multiple start vertices. Used for DFS - and for this, it is equipped with forward, back, and cross edges and with already visited start vertices. @@ -535,7 +555,7 @@ def __init__(self) -> None: self.goal_vertices = (4,) -class FMultiStartAStar(FixtureFull[int, tuple[int, int, int]]): +class FMultiStartAStar(FixtureFull[int, int, int]): """Graph for testing multiple start vertices. Used for all strategies except of A* and the bidirectional search strategies.""" @@ -550,7 +570,7 @@ def __init__(self) -> None: self.start_vertices = (0, 1) -class FMultiStartB(FixtureFull[int, tuple[int, int, int]]): +class FMultiStartB(FixtureFull[int, int, int]): """Graph for testing multiple start vertices. Used for bidirectional search strategies.""" @@ -575,7 +595,7 @@ def __init__(self) -> None: self.start_vertices_bi = (self.start_vertices, self.goal_vertices) -class FSpiral(Fixture[int, tuple[int, int, int]]): +class FSpiral(Fixture[int, int, int]): """Graph for testing TraversalShortestPathsFlex with all gears. Outgoing edges are sorted by ascending weight. """ @@ -605,16 +625,17 @@ def next_edges(i: int, _: Any) -> Iterable[tuple[int, int, int]]: return out_edges -class FOvertaking(FixtureFull[int, tuple[int, int, int, int]]): +class FOvertaking(FixtureFull[int, int, int]): """Graph for testing all strategies with different gears. It can be used to create a distance overflow for distance values stored in an array of - byte. It contains no cycles, which is required for topological search.""" + byte, even for a bidirectional search for shortest paths. It contains no + cycles, which is required for topological search.""" def __init__(self) -> None: _enough_for_index_error = (1 + 128) * 8 # index error even for seq of bits - goal = 2 * _enough_for_index_error - limit = 3 * _enough_for_index_error - self.last_vertex = limit + 2 # todo: Goal? Remove? + goal = 3 * _enough_for_index_error + limit = 4 * _enough_for_index_error + self.last_vertex = limit + 2 edges = [] for v in range(0, limit, 2): @@ -627,7 +648,7 @@ def __init__(self) -> None: super().__init__(edges, 0, goal, lambda v: 0, report=False) -class FOvertakingDFSWithBackEdges(FixtureFull[int, tuple[int, int, int, int]]): +class FOvertakingDFSWithBackEdges(FixtureFull[int, int, int]): """Graph for testing DFS with different gears. It can be used to create a distance overflow for distance values stored in an array of byte. And it has back edges, which is required for a full DFS test.""" @@ -636,7 +657,7 @@ def __init__(self) -> None: _enough_for_index_error = (1 + 128) * 8 # index error even for seq of bits goal = 2 * _enough_for_index_error limit = 3 * _enough_for_index_error - self.last_vertex = limit + 2 # todo: Goal? Remove? + self.last_vertex = limit + 2 edges = [] for v in range(0, limit, 2): @@ -650,7 +671,7 @@ def __init__(self) -> None: super().__init__(edges, 0, goal, lambda v: 0, report=False) -class FSequenceTo255(Fixture[int, tuple[int, int]]): +class FSequenceTo255(Fixture[int, Any, Any]): """Graph with vertices from 0 to 255, edges from i to i+1, and 255 as goal. It can be used to create an infinity overflow of guessed distance (guess >= 255) for distance guess values stored in an array of byte. @@ -669,7 +690,7 @@ def heuristic(vertex: int) -> int: return 255 - vertex # Create infinity overflow of guessed distance -class FBinaryTreeFixedWeights(Fixture[int, tuple[int, int, int]]): +class FBinaryTreeFixedWeights(Fixture[int, int, int]): """Graph for testing strategies with is_tree and different gears.""" def __init__(self) -> None: @@ -3232,8 +3253,8 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: >>> print(l, f.goal not in p) 3 True >>> l, p = nog.BSearchBreadthFirst(fb.next_vertices_bi - ... ).start_from(start_and_goal_vertices=[ - ... iter(s) for s in fb.start_vertices_bi], build_path=True) + ... ).start_from(start_and_goal_vertices=tuple( + ... iter(s) for s in fb.start_vertices_bi), build_path=True) ? 0: {'depth': 0, 'visited': {0, 5}, 'paths': {0: (0,)}} ? 5: {'depth': 0, 'visited': {0, 1, 5}, 'paths': {5: (5,)}} ?<4: {'depth': 0, 'visited': {4}, 'paths': {4: (4,)}} @@ -3624,7 +3645,7 @@ class MultipleStartVerticesTraversalsWithWeights: >>> print(l, fb.goal not in p) 4 True >>> l, p = search.start_from( - ... start_and_goal_vertices = [iter(s) for s in fb.start_vertices_bi], + ... start_and_goal_vertices = tuple(iter(s) for s in fb.start_vertices_bi), ... build_path=True) ? 5: {} ?<4: {} @@ -3805,135 +3826,6 @@ class IllegalParameters: """ -class InitiationForgotten: - """Check if the library detects the mistake that start_from or one of the - go_... methods are called on a traversal class instead of an object, i.e., the - round brackets after the class name have been forgotten. - - >>> nog.TraversalBreadthFirst.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalBreadthFirst.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalBreadthFirst.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalBreadthFirst.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - >>> nog.TraversalBreadthFirst.go_for_depth_range(None, None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_depth_range can only be called on a Traversal object. - - >>> nog.TraversalDepthFirst.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalDepthFirst.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalDepthFirst.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalDepthFirst.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - - >>> nog.TraversalNeighborsThenDepth.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalNeighborsThenDepth.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalNeighborsThenDepth.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalNeighborsThenDepth.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - - >>> nog.TraversalShortestPaths.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalShortestPaths.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalShortestPaths.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalShortestPaths.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - >>> nog.TraversalShortestPaths.go_for_distance_range(None, None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_distance_range can only be called on a Traversal object. - - >>> nog.TraversalShortestPathsInfBranchingSorted.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalShortestPathsInfBranchingSorted.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalShortestPathsInfBranchingSorted.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalShortestPathsInfBranchingSorted.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - >>> nog.TraversalShortestPathsInfBranchingSorted.go_for_distance_range( - ... None, None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_distance_range can only be called on a Traversal object. - - >>> nog.TraversalAStar.start_from(None, None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalAStar.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalAStar.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalAStar.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - - >>> nog.TraversalMinimumSpanningTree.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalMinimumSpanningTree.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalMinimumSpanningTree.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalMinimumSpanningTree.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - - >>> nog.TraversalTopologicalSort.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a Traversal object. - >>> nog.TraversalTopologicalSort.__iter__(None) - Traceback (most recent call last): - RuntimeError: Method go can only be called on a Traversal object. - >>> nog.TraversalTopologicalSort.go_to(None, None) - Traceback (most recent call last): - RuntimeError: Method go_to can only be called on a Traversal object. - >>> nog.TraversalTopologicalSort.go_for_vertices_in(None, None) - Traceback (most recent call last): - RuntimeError: Method go_for_vertices_in can only be called on a Traversal object. - - >>> nog.BSearchBreadthFirst.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a search strategy object. - - >>> nog.BSearchShortestPath.start_from(None) - Traceback (most recent call last): - RuntimeError: Method start_from can only be called on a search strategy object. - """ - - class RandomExample: """ Checks based on random example graph @@ -4144,26 +4036,26 @@ class GearTestsTraversalsWithOrWithoutLabels: >>> for t in gear_test_traversals(nog.TraversalBreadthFirstFlex, f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), ... paths_to=f.goal) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), ... paths_to=f.goal) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) For DFS without trace and events, we also test without paths, because this changes the process @@ -4183,25 +4075,25 @@ class GearTestsTraversalsWithOrWithoutLabels: ... f.start, build_paths=True, compute_on_trace=True), ... paths_to=f.goal) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f.next_edges): ... print_partial_results(t.start_from( ... f.start, build_paths=True, compute_index=True), ... paths_to=f.goal) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) Now, we also report NON_TREE_EDGESs, since this enables and uses the functionality of options index and on_trace @@ -4211,13 +4103,13 @@ class GearTestsTraversalsWithOrWithoutLabels: ... report=nog.DFSEvent.ENTERING_SUCCESSOR | nog.DFSEvent.NON_TREE_EDGES), ... paths_to=f.goal) [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f2.next_edges): ... print_partial_results(t.start_from( @@ -4238,48 +4130,48 @@ class GearTestsTraversalsWithOrWithoutLabels: ... f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), ... paths_to=f.goal) - [1, 3, 6, 4, 5] [3093, 3095, 3098, 3096, 3097, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) - [1, 3, 6, 4, 5] [3093, 3095, 3098, 3096, 3097, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) - [1, 3, 6, 4, 5] [3093, 3095, 3098, 3096, 3097, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) - [1, 3, 6, 4, 5] [3093, 3095, 3098, 3096, 3097, 2] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [1, 3, 6, 4, 5] [4125, 4127, 4130, 4128, 4129, 2] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) + [1, 3, 6, 4, 5] [4125, 4127, 4130, 4128, 4129, 2] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) + [1, 3, 6, 4, 5] [4125, 4127, 4130, 4128, 4129, 2] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) + [1, 3, 6, 4, 5] [4125, 4127, 4130, 4128, 4129, 2] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) >>> for t in gear_test_traversals(nog.TraversalTopologicalSortFlex, ... f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), ... paths_to=f.goal) - [3096, 3098, 3095, 3097, 3094] [5, 4, 3, 2, 1, 0] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) - [3096, 3098, 3095, 3097, 3094] [5, 4, 3, 2, 1, 0] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) - [3096, 3098, 3095, 3097, 3094] [5, 4, 3, 2, 1, 0] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) - [3096, 3098, 3095, 3097, 3094] [5, 4, 3, 2, 1, 0] - ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [4128, 4130, 4127, 4129, 4126] [5, 4, 3, 2, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) + [4128, 4130, 4127, 4129, 4126] [5, 4, 3, 2, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) + [4128, 4130, 4127, 4129, 4126] [5, 4, 3, 2, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) + [4128, 4130, 4127, 4129, 4126] [5, 4, 3, 2, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((3092, 3095, 3), (3095, 3096, 3)) >>> for t in gear_test_traversals(nog.TraversalShortestPathsFlex, ... f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), ... paths_to=f.goal) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) >>> t = nog.TraversalShortestPathsFlex( ... nog.vertex_as_id, ... nog.GearForIntVerticesAndIDsAndCInts(), ... next_labeled_edges=f.next_edges) >>> print_partial_results(t.start_from(f.start, build_paths=True), ... paths_to=f.goal) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) >>> t = nog.TraversalShortestPathsFlex( ... nog.vertex_as_id, ... nog.GearForIntVerticesAndIDsAndCInts(distance_type_code="B"), @@ -4295,27 +4187,27 @@ class GearTestsTraversalsWithOrWithoutLabels: ... f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), ... paths_to=f.goal) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [1, 3, 4, 2, 6] [3092, 3094, 3096, 3095, 3097, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [1, 3, 4, 2, 6] [4124, 4126, 4128, 4127, 4129, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) >>> for t in gear_test_traversals(nog.TraversalAStarFlex, f.next_edges): ... print_partial_results( ... t.start_from(f.heuristic, f.start, build_paths=True), ... paths_to=f.goal) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) >>> t = nog.TraversalAStarFlex( ... nog.vertex_as_id, ... nog.GearForIntVerticesAndIDsAndCInts(), # infinity overflow in distance @@ -4323,8 +4215,8 @@ class GearTestsTraversalsWithOrWithoutLabels: >>> print_partial_results( ... t.start_from(f.heuristic, f.start, build_paths=True), ... paths_to=f.goal) - [3, 1, 2, 4, 6] [3092, 3094, 3096, 3097, 3095, 3098] - ((0, 3, 3), (3, 6, 1)) ((2058, 2061, 3), (2061, 2064, 1)) + [3, 1, 2, 4, 6] [4124, 4126, 4128, 4129, 4127, 4130] + ((0, 3, 3), (3, 6, 1)) ((3090, 3093, 3), (3093, 3096, 1)) >>> t = nog.TraversalAStarFlex( ... nog.vertex_as_id, ... nog.GearForIntVerticesAndIDsAndCInts(distance_type_code="B"), @@ -4440,42 +4332,42 @@ class GearTestsTraversalsWithOrWithoutLabels: >>> v = t.start_from(f.start, build_paths=True).go_to(f.last_vertex) >>> path = t.paths[f.last_vertex] >>> print(t.depth, path[:5], path[-5:], f.last_vertex) - 1034 (0, 1, 4, 5, 8) (3086, 3089, 3092, 3095, 3098) 3098 + 1378 (0, 1, 4, 5, 8) (4118, 4121, 4124, 4127, 4130) 4130 >>> for g in test_gears: ... test_for_gear(nog.BSearchBreadthFirstFlex, g) -- GearForHashableVertexIDsAndIntsMaybeFloats -- - 1034 (0, 1, 4, 5, 8) (3086, 3089, 3092, 3095, 3098) - (0, 1, 4) (3095, 3098) - (3098, 3095, 3092) (1, 0) - ((0, 1), (1, 4), (4, 5)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((1, 4), (0, 1)) - ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((1, 4, 1), (0, 1, 1)) + 1378 (0, 1, 4, 5, 8) (4118, 4121, 4124, 4127, 4130) + (0, 1, 4) (4127, 4130) + (4130, 4127, 4124) (1, 0) + ((0, 1), (1, 4), (4, 5)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((1, 4), (0, 1)) + ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((1, 4, 1), (0, 1, 1)) -- GearForIntVertexIDsAndCFloats -- - 1034 (0, 1, 4, 5, 8) (3086, 3089, 3092, 3095, 3098) - (0, 1, 4) (3095, 3098) - (3098, 3095, 3092) (1, 0) - ((0, 1), (1, 4), (4, 5)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((1, 4), (0, 1)) - ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((1, 4, 1), (0, 1, 1)) + 1378 (0, 1, 4, 5, 8) (4118, 4121, 4124, 4127, 4130) + (0, 1, 4) (4127, 4130) + (4130, 4127, 4124) (1, 0) + ((0, 1), (1, 4), (4, 5)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((1, 4), (0, 1)) + ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((1, 4, 1), (0, 1, 1)) -- GearForIntVerticesAndIDsAndCFloats -- - 1034 (0, 1, 4, 5, 8) (3086, 3089, 3092, 3095, 3098) - (0, 1, 4) (3095, 3098) - (3098, 3095, 3092) (1, 0) - ((0, 1), (1, 4), (4, 5)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((1, 4), (0, 1)) - ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((1, 4, 1), (0, 1, 1)) + 1378 (0, 1, 4, 5, 8) (4118, 4121, 4124, 4127, 4130) + (0, 1, 4) (4127, 4130) + (4130, 4127, 4124) (1, 0) + ((0, 1), (1, 4), (4, 5)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((1, 4), (0, 1)) + ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((1, 4, 1), (0, 1, 1)) -- GearForIntVerticesAndIDsAndCFloats -- - 1034 (0, 1, 4, 5, 8) (3086, 3089, 3092, 3095, 3098) - (0, 1, 4) (3095, 3098) - (3098, 3095, 3092) (1, 0) - ((0, 1), (1, 4), (4, 5)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((1, 4), (0, 1)) - ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((1, 4, 1), (0, 1, 1)) + 1378 (0, 1, 4, 5, 8) (4118, 4121, 4124, 4127, 4130) + (0, 1, 4) (4127, 4130) + (4130, 4127, 4124) (1, 0) + ((0, 1), (1, 4), (4, 5)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((1, 4), (0, 1)) + ((0, 1, 1), (1, 4, 1), (4, 5, 1)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((1, 4, 1), (0, 1, 1)) For testing BidirectionalSearchShortestPath, we use the length of the @@ -4484,53 +4376,53 @@ class GearTestsTraversalsWithOrWithoutLabels: >>> v = t.start_from(f.start, build_paths=True).go_to(f.last_vertex) >>> path = t.paths[f.last_vertex] >>> print(t.distance, path[:5], path[-5:], f.last_vertex) - 1034 (0, 3, 6, 9, 12) (3088, 3091, 3092, 3095, 3098) 3098 + 1378 (0, 3, 6, 9, 12) (4120, 4123, 4124, 4127, 4130) 4130 >>> for g in test_gears: ... test_for_gear(nog.BSearchShortestPathFlex, g) -- GearForHashableVertexIDsAndIntsMaybeFloats -- - 1034 (0, 3, 6, 9, 12) (3086, 3089, 3092, 3095, 3098) - (0, 3, 6) (3095, 3098) - (3098, 3095, 3092) (3, 0) - ((0, 3), (3, 6), (6, 9)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((3, 6), (0, 3)) - ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((3, 6, 1), (0, 3, 3)) + 1378 (0, 3, 6, 9, 12) (4118, 4121, 4124, 4127, 4130) + (0, 3, 6) (4127, 4130) + (4130, 4127, 4124) (3, 0) + ((0, 3), (3, 6), (6, 9)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((3, 6), (0, 3)) + ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((3, 6, 1), (0, 3, 3)) -- GearForIntVertexIDsAndCFloats -- - 1034.0 (0, 3, 6, 9, 12) (3086, 3089, 3092, 3095, 3098) - (0, 3, 6) (3095, 3098) - (3098, 3095, 3092) (3, 0) - ((0, 3), (3, 6), (6, 9)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((3, 6), (0, 3)) - ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((3, 6, 1), (0, 3, 3)) + 1378.0 (0, 3, 6, 9, 12) (4118, 4121, 4124, 4127, 4130) + (0, 3, 6) (4127, 4130) + (4130, 4127, 4124) (3, 0) + ((0, 3), (3, 6), (6, 9)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((3, 6), (0, 3)) + ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((3, 6, 1), (0, 3, 3)) -- GearForIntVerticesAndIDsAndCFloats -- - 1034.0 (0, 3, 6, 9, 12) (3086, 3089, 3092, 3095, 3098) - (0, 3, 6) (3095, 3098) - (3098, 3095, 3092) (3, 0) - ((0, 3), (3, 6), (6, 9)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((3, 6), (0, 3)) - ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((3, 6, 1), (0, 3, 3)) + 1378.0 (0, 3, 6, 9, 12) (4118, 4121, 4124, 4127, 4130) + (0, 3, 6) (4127, 4130) + (4130, 4127, 4124) (3, 0) + ((0, 3), (3, 6), (6, 9)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((3, 6), (0, 3)) + ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((3, 6, 1), (0, 3, 3)) -- GearForIntVerticesAndIDsAndCFloats -- - 1034.0 (0, 3, 6, 9, 12) (3086, 3089, 3092, 3095, 3098) - (0, 3, 6) (3095, 3098) - (3098, 3095, 3092) (3, 0) - ((0, 3), (3, 6), (6, 9)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((3, 6), (0, 3)) - ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((3, 6, 1), (0, 3, 3)) + 1378.0 (0, 3, 6, 9, 12) (4118, 4121, 4124, 4127, 4130) + (0, 3, 6) (4127, 4130) + (4130, 4127, 4124) (3, 0) + ((0, 3), (3, 6), (6, 9)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((3, 6), (0, 3)) + ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((3, 6, 1), (0, 3, 3)) >>> test_for_gear(nog.BSearchShortestPathFlex, ... nog.GearForIntVerticesAndIDsAndCInts()) -- GearForIntVerticesAndIDsAndCInts -- - 1034 (0, 3, 6, 9, 12) (3086, 3089, 3092, 3095, 3098) - (0, 3, 6) (3095, 3098) - (3098, 3095, 3092) (3, 0) - ((0, 3), (3, 6), (6, 9)) ((3092, 3095), (3095, 3098)) - ((3095, 3098), (3092, 3095), (3089, 3092)) ((3, 6), (0, 3)) - ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((3092, 3095, 3), (3095, 3098, 1)) - ((3095, 3098, 1), (3092, 3095, 3), (3089, 3092, 1)) ((3, 6, 1), (0, 3, 3)) + 1378 (0, 3, 6, 9, 12) (4118, 4121, 4124, 4127, 4130) + (0, 3, 6) (4127, 4130) + (4130, 4127, 4124) (3, 0) + ((0, 3), (3, 6), (6, 9)) ((4124, 4127), (4127, 4130)) + ((4127, 4130), (4124, 4127), (4121, 4124)) ((3, 6), (0, 3)) + ((0, 3, 3), (3, 6, 1), (6, 9, 3)) ((4124, 4127, 3), (4127, 4130, 1)) + ((4127, 4130, 1), (4124, 4127, 3), (4121, 4124, 1)) ((3, 6, 1), (0, 3, 3)) >>> test_for_gear(nog.BSearchShortestPathFlex, ... nog.GearForIntVerticesAndIDsAndCInts(distance_type_code="B") diff --git a/tests/test_types.py b/tests/test_types.py deleted file mode 100644 index 7d1f50a..0000000 --- a/tests/test_types.py +++ /dev/null @@ -1,40 +0,0 @@ -import nographs as nog # noqa: F401 (used only by doctests) - - -class ProtocolAndABCNotImplementedErrors: - """-- Abstract methods of protocols and ABCs. - - If the application calls them and ignores that they are abstract, an assertion - is to be raised to inform the application about its mistake. - Check, if this mechanism is correctly implemented. - - Note: The following calls are all illegal w.r.t. typing (only the number of - parameters is correct): Instance methods are called like a classmethod would, - the given argument for parameter self has the wrong type, and other arguments may - be illegal, too, and the generic parameters are missing. But all this does not - matter here, since the methods are to raise NotImplementedError directly and in - all cases. - - >>> nog.Weight.__add__(0, 0) - Traceback (most recent call last): - NotImplementedError - - >>> nog.Weight.__sub__(0, 0) - Traceback (most recent call last): - NotImplementedError - - >>> nog.Weight.__lt__(0, 0) - Traceback (most recent call last): - NotImplementedError - >>> nog.Weight.__le__(0, 0) - Traceback (most recent call last): - NotImplementedError""" - - -class Functionality: - """Test function vertex_as_id. Test needed here, since this - function is not intended to be really called anywhere in NoGraphs. - - >>> nog.vertex_as_id(0) - 0 - """ diff --git a/tests/test_unit_traversal_and_searches.py b/tests/test_unit_traversal_and_searches.py new file mode 100644 index 0000000..a85165b --- /dev/null +++ b/tests/test_unit_traversal_and_searches.py @@ -0,0 +1,122 @@ +import unittest +import nographs as nog + + +# --- Tests --- + + +class InitiationForgotten(unittest.TestCase): + """Check if the library detects the mistake that start_from or one of the + go_... methods are called on a traversal class instead of an object, i.e., the + round brackets after the class name have been forgotten. + + If the detection works correctly, CPython and MyPy raise a runtime error + and MyPyC raises a TypeError. + We hide the problem from MyPy here, because we make the error on purpose: + Testing the runtime detection of these errors is the purpose of the tests. + """ + + # todo: NoGraphs´ own detection should also raise TypeError + def test_TraversalBreadthFirst(self) -> None: + cls = nog.TraversalBreadthFirst + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_depth_range(None, None, None) # type: ignore [arg-type] + + def test_TraversalDepthFirst(self) -> None: + cls = nog.TraversalDepthFirst + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + + def test_TraversalNeighborsThenDepth(self) -> None: + cls = nog.TraversalNeighborsThenDepth + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + + def test_TraversalShortestPaths(self) -> None: + cls = nog.TraversalShortestPaths + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_distance_range(None, None, None) # type: ignore [arg-type] + + def test_TraversalShortestPathsInfBranchingSorted(self) -> None: + cls = nog.TraversalShortestPathsInfBranchingSorted + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_distance_range(None, None, None) # type: ignore [arg-type] + + def test_TraversalAStar(self) -> None: + cls = nog.TraversalAStar + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None, None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + + def test_TraversalMinimumSpanningTree(self) -> None: + cls = nog.TraversalMinimumSpanningTree + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + + def test_TraversalTopologicalSort(self) -> None: + cls = nog.TraversalTopologicalSort + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.__iter__(None) # type: ignore [arg-type] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_to(None, None) # type: ignore [call-overload] + with self.assertRaises((RuntimeError, TypeError)): + cls.go_for_vertices_in(None, None) # type: ignore [arg-type] + + def test_BSearchBreadthFirst(self) -> None: + cls = nog.BSearchBreadthFirst + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] + + def test_BSearchShortestPath(self) -> None: + cls = nog.BSearchShortestPath + with self.assertRaises((RuntimeError, TypeError)): + cls.start_from(None) # type: ignore [arg-type] diff --git a/tests/test_unit_typed.py b/tests/test_unit_typed.py index e124408..df017dd 100644 --- a/tests/test_unit_typed.py +++ b/tests/test_unit_typed.py @@ -104,7 +104,7 @@ def test_tsp_typing_docs_example(self) -> None: def test_dfs_throw(self) -> None: """DFS function iter() returns a Collections.Generator. Its type is tested - my the MyPy run here. And The behaviour of the throw, both for stops + by the MyPy run here. And The behaviour of the throw, both for stops at a start vertex and at other vertices.""" def next_vertices(i: int, _: Any) -> Iterator[int]: @@ -140,6 +140,10 @@ def next_vertices2(i: int, _: Any) -> Iterable[int]: try: # noinspection PyUnresolvedReferences _ = it.throw(stop) - except RuntimeError: + # CPython and PyPy: Following peps.python.org/pep-0479, the StopIteration is + # converted to a RuntimeError. + # PyPyC: The StopIteration falls through. This is documented in the tutorial + # of NoGraphs. + except (RuntimeError, StopIteration): return self.fail("StopIteration, thrown at illegal moment, raises RuntimeError.") diff --git a/tests/test_unit_types.py b/tests/test_unit_types.py new file mode 100644 index 0000000..f81dc6a --- /dev/null +++ b/tests/test_unit_types.py @@ -0,0 +1,33 @@ +import unittest +import nographs as nog + + +class TestWeight(unittest.TestCase): + """Check if the library detects the mistake that abstract methods + of a protocol are called. + + If the detection works correctly, CPython and MyPy raise a runtime error + and MyPyC raises a TypeError. + We hide the problem from MyPy here, because we make the error on purpose: + Testing the runtime detection of these errors is the purpose of the tests. + """ + + def test_protocol_and_ABC_not_implemented_errors(self) -> None: + cls = nog.Weight + with self.assertRaises((NotImplementedError, TypeError)): + cls.__add__(0, 0) + with self.assertRaises((NotImplementedError, TypeError)): + cls.__sub__(0, 0) + with self.assertRaises((NotImplementedError, TypeError)): + cls.__lt__(0, 0) + with self.assertRaises((NotImplementedError, TypeError)): + cls.__le__(0, 0) + + +class TestVertex_AS_ID(unittest.TestCase): + """Test function vertex_as_id. Test needed here, since this + function is not intended to be really called anywhere in NoGraphs. + """ + + def test_functionality(self) -> None: + self.assertEqual(nog.vertex_as_id(0), 0) diff --git a/tpl/src/nographs/_strategies/MVertexSet.py b/tpl/src/nographs/_strategies/MVertexSet.py index e64373b..53e2f49 100644 --- a/tpl/src/nographs/_strategies/MVertexSet.py +++ b/tpl/src/nographs/_strategies/MVertexSet.py @@ -47,16 +47,22 @@ def combine_access(collection1: str, collection2: str, combined: str): + "with collection {collection2}" ) {combined}_uses_sequence = {collection1}_uses_sequence - del {collection1}_uses_sequence, {collection2}_uses_sequence + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del {collection1}_uses_sequence, {collection2}_uses_sequence {combined}_uses_bits = {collection1}_uses_bits - del {collection1}_uses_bits, {collection2}_uses_bits + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del {collection1}_uses_bits, {collection2}_uses_bits if {combined}_uses_sequence and {combined}_uses_bits: assert {collection1}_index_and_bit_method is {collection2}_index_and_bit_method, ( "Collection {collection1} is incompatible " + "with collection {collection2}" ) {combined}_index_and_bit_method = {collection1}_index_and_bit_method - del {collection1}_index_and_bit_method, {collection2}_index_and_bit_method + # MyPyC: Deleting local variables is not allowed. It has been deleted + # to ensure that it is not used from here on. + # del {collection1}_index_and_bit_method, {collection2}_index_and_bit_method """ ) diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py b/tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py index 42ccdf0..3448e07 100644 --- a/tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py +++ b/tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py @@ -1,9 +1,6 @@ -from __future__ import annotations - import itertools from heapq import heapify, heappop, heappush -from numbers import Real -from typing import Optional, Any, Generic, Union +from typing import Optional, Any, Generic, Union, ClassVar from collections.abc import Callable, Iterable, Generator from nographs._types import ( @@ -74,6 +71,8 @@ class TraversalAStarFlex( *path_length*, *depth*, *paths*. """ + _state_attrs: ClassVar = _TraversalWithDistances._state_attrs + ["path_length", "depth"] + def __init__( self, # $$ MStrategyWithWeights.init_signature('TraversalAStarFlex') @@ -96,7 +95,7 @@ def __init__( """ self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() - self._heuristic: Optional[Callable[[T_vertex], Real]] = None + self._heuristic: Optional[Callable[[T_vertex], T_weight]] = None self._known_distances: Optional[ VertexIdToDistanceMapping[T_vertex_id, T_weight] ] = None @@ -109,7 +108,7 @@ def __init__( def start_from( self, - heuristic: Callable[[T_vertex], Real], + heuristic: Callable[[T_vertex], T_weight], # $$ insert_from('$$/method_start_from/signature_standard.py') known_distances: Optional[ VertexIdToDistanceMapping[T_vertex_id, T_weight] @@ -117,7 +116,7 @@ def start_from( known_path_length_guesses: Optional[ VertexIdToDistanceMapping[T_vertex_id, T_weight] ] = None, - ) -> TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + ) -> "TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py b/tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py index 479e302..cdfe605 100644 --- a/tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py +++ b/tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import itertools from heapq import heapify, heappop, heappush -from typing import Optional, Any, Generic, Union +from typing import Optional, Any, Generic, Union, ClassVar from collections.abc import Iterable, Generator from nographs._types import ( @@ -60,6 +58,8 @@ class TraversalMinimumSpanningTreeFlex( *edge* and *paths*. """ + _state_attrs: ClassVar = _TraversalWithWeights._state_attrs + ["edge"] + def __init__( self, # $$ MStrategyWithWeights.init_signature('TraversalMinimumSpanningTreeFlex') @@ -78,7 +78,7 @@ def __init__( def start_from( self, # $$ insert_from('$$/method_start_from/signature_standard.py') - ) -> TraversalMinimumSpanningTreeFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + ) -> "TraversalMinimumSpanningTreeFlex[T_vertex, T_vertex_id, T_weight, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. If you provide more than one start vertex, the result consists of several diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py b/tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py index f88f48d..c181587 100644 --- a/tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py +++ b/tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import itertools from heapq import heapify, heappop, heappush -from typing import Optional, Any, Generic, Union +from typing import Optional, Any, Generic, Union, ClassVar from collections.abc import Iterable, Generator from nographs._gears import VertexIdToDistanceMapping @@ -60,6 +58,8 @@ class TraversalShortestPathsFlex( *distance*, *depth*, *paths*, and *distances*. """ + _state_attrs: ClassVar = _TraversalWithDistance._state_attrs + ["depth"] + def __init__( self, # $$ MStrategyWithWeights.init_signature('TraversalShortestPathsFlex') @@ -92,7 +92,7 @@ def start_from( known_distances: Optional[ VertexIdToDistanceMapping[T_vertex_id, T_weight] ] = None, - ) -> TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + ) -> "TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels]": """ Start the traversal at a vertex or a set of vertices and set parameters. diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py b/tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py index 959bcb2..f8a1032 100644 --- a/tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py +++ b/tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py @@ -10,13 +10,15 @@ class MStrategyWithWeights: @staticmethod def init_signature(traversal_type: str) -> None: insert( - f"""\ + f'''\ vertex_to_id: VertexToID[T_vertex, T_vertex_id], gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], next_edges: Optional[ NextWeightedEdges[ T_vertex, - {traversal_type}[T_vertex, T_vertex_id, T_weight, T_labels], + """{traversal_type}[ + T_vertex, T_vertex_id, T_weight, T_labels + ]""", T_weight, ] ] = None, @@ -24,12 +26,14 @@ def init_signature(traversal_type: str) -> None: next_labeled_edges: Optional[ NextWeightedLabeledEdges[ T_vertex, - {traversal_type}[T_vertex, T_vertex_id, T_weight, T_labels], + """{traversal_type}[ + T_vertex, T_vertex_id, T_weight, T_labels + ]""", T_weight, T_labels, ] ] = None, -""" +''' ) @staticmethod @@ -87,6 +91,8 @@ class {class_name} ( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = {class_name}Flex._state_attrs + def __init__( self, next_edges: Optional[ diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py b/tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py index 1278d6f..4d765db 100644 --- a/tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py @@ -1,7 +1,4 @@ -from __future__ import annotations - -import copy -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Iterator, Generator @@ -66,6 +63,8 @@ class TraversalBreadthFirstFlex( *depth*, *paths*, and *visited*. """ + _state_attrs: ClassVar = _TraversalWithoutWeightsWithVisited._state_attrs + ["depth"] + def __init__( self, # $$ MStrategyWithoutWeights.init_signature('TraversalBreadthFirstFlex') @@ -73,11 +72,43 @@ def __init__( "$$ MStrategyWithoutWeights.init_code(search_depth_is_vertex_depth=True) $$" self._report_depth_increase = False + def _copy( + self, + ) -> "TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]": + """Return a copy of the current traversal. + + (We cannot use copy.copy() instead, because PyPyC has a (documented) + incompatibility in the semantics of this function. + """ + t = TraversalBreadthFirstFlex( + self._vertex_to_id, self._gear, lambda v, t: [], + is_tree=self._is_tree + ) + # Attributes of Traversal + # (except for _is_tree and _vertex_to_id that we have already set) + t._labeled_edges = self._labeled_edges + t._generator = self._generator + t._start_vertices = self._start_vertices + t._build_paths = self._build_paths + t._calculation_limit = self._calculation_limit + t.paths = self.paths + t._predecessors = self._predecessors + t._attributes = self._attributes + # Attributes of _TraversalWithoutWeights + # (except for _gear that we have already set) + t._edges_with_data = self._edges_with_data + # Attributes of _TraversalWithoutWeightsWIthVisited + t.visited = self.visited + # Attributes of TraversalBreadthFirstFlex + t.depth = self.depth + t._report_depth_increase = self._report_depth_increase + return t + def start_from( self, # $$ insert_from('$$/method_start_from/signature.py') _report_depth_increase: bool = False, # hidden parameter for internal use - ) -> TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]": """ # $$ insert_from('$$/method_start_from/doc_start.rst') # $$ insert_from('$$/method_start_from/doc_already_visited_std.txt') @@ -108,7 +139,7 @@ def _traverse(self) -> Generator[T_vertex, None, Any]: # one, while for the latter, it needs to be one higher. In order to avoid # a cascade of +1 and -1 on the depth, we just use a copy of the traversal, # that hangs by one in the depth, and give this to next_edge_or_vertices. - prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + prev_traversal = self._copy() # copy of self, for keeping previous depth self.depth = 1 # used for reporting (prev_traversal starts at 0) # Get method references of specific bookkeeping (avoid attribute resolution) diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py b/tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py index 69b4556..9b40d8c 100644 --- a/tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py @@ -1,9 +1,7 @@ -from __future__ import annotations - import array -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Generator -from enum import Flag, auto +# from enum import Flag, auto from nographs._types import ( T_vertex, @@ -39,137 +37,12 @@ _TraversalWithoutWeightsWithVisited, ) +from .depth_first_enum_types import DFSEvent, DFSMode + "$$ import_from('$$/MTraversalWithoutWeights.py') $$" "$$ import_from('$$/../../MVertexMapping.py') $$" -class DFSEvent(Flag): - """ - An enumeration of the events that can trigger the report of a vertex / edge by - TraversalDepthFirst. - - Events reporting that a vertex is entered or left: - - - ENTERING_START: A start vertex has been entered and the traversal starts - there. - - - LEAVING_START: A start vertex has been left (the traversal may continue - with the next one). - - - ENTERING_SUCCESSOR: A vertex is entered, when an edge - that leads to it is followed. In mode *DFS_TREE*, only *DFS-tree edges* - are followed. - - - LEAVING_SUCCESSOR: A vertex is left, when an edge that leads to it - and has been followed, is now followed in reversed direction, during - backtracking from the edge. In mode *DFS_TREE*, only *DFS-tree edges* - are followed. - - Events reporting that a vertex (or an edge) has been detected but will not be - entered (resp. followed): - - - SKIPPING_START: A start vertex was about to be entered, as start of a - traversal from there, but it has already been visited as descendant of - another start vertex, and thus, it is skipped. - - - BACK_EDGE: An edge *(u, v)* is found, where v has already been entered, - but not left so far. In other words, *v* is on the trace (path that - leads to *u* within the tree). - - - FORWARD_EDGE: An edge *(u, v)* is found, where *v* has already been - left, and it had been entered after *u*. *(u, v)* is a shortcut - forwards in the tree branch from *u* to *v*, so to speak. - - - CROSS_EDGE: An edge *(u, v)* is found, where *v* has already been left, - and it had been entered before *u*. This means, in the DFS tree, - *u* and *v* do not have any ancestor or descendant relationship - between them. - - Events that combine other events as a group (*group-events*): - - - SOME_NON_TREE_EDGE: One of the events FORWARD_EDGE, BACK_EDGE, or CROSS_EDGE - occurred, but it has not been determined which of these events. - - - FORWARD_OR_CROSS_EDGE: One of the events FORWARD_EDGE or CROSS_EDGE - occurred, but it has not been determined which of these events. - - - Aliases for sets of events: - - - NONE = 0 - - - ENTERING = ENTERING_START | ENTERING_SUCCESSOR - - LEAVING = LEAVING_START | LEAVING_SUCCESSOR - - - IN_OUT_START = ENTERING_START | LEAVING_START - - IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR - - IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR - - - NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE - - EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES - - - ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES - """ - - ENTERING_START = auto() - LEAVING_START = auto() - - ENTERING_SUCCESSOR = auto() - LEAVING_SUCCESSOR = auto() - - SKIPPING_START = auto() - BACK_EDGE = auto() - FORWARD_EDGE = auto() - CROSS_EDGE = auto() - - SOME_NON_TREE_EDGE = auto() - FORWARD_OR_CROSS_EDGE = auto() - - NONE = 0 - - ENTERING = ENTERING_START | ENTERING_SUCCESSOR - LEAVING = LEAVING_START | LEAVING_SUCCESSOR - - IN_OUT_START = ENTERING_START | LEAVING_START - IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR - IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR - - NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE - EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES - - ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES - - -class DFSMode(Flag): - """ - An enumeration of the traversing mode to be used by TraversalDepthFirst. - - The modes are: - - - DFS_TREE: The traversal follows the edges of the DFS tree. If demanded, - non-tree edges are reported, but not followed. Vertices are only - visited once. - - - ALL_PATHS: A simple path is a path that does not contain a vertex twice. - In this mode, the traversal follows all edges, also edges leading to - vertices that have already been visited. But edges to vertices, that are - already on the trace (current path from a start vertex to the current - vertex) are ignored. For example, this can be used to search in the set - of all possible simple paths from some edges to some others. - - - ALL_WALKS: A walk is a sequence of nodes in which each adjacent pair of - nodes in the sequence is adjacent in the graph. - A walk can contain the same vertex or edge more than once. - In this more, the traversal follows all edges, also edges leading to - vertices that have already been followed as part of the trace (the - current walk from a start vertex to the current vertex). - """ - - DFS_TREE = auto() - ALL_PATHS = auto() - ALL_WALKS = auto() - - class TraversalDepthFirstFlex( _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] ): @@ -200,6 +73,11 @@ class TraversalDepthFirstFlex( *on_trace*, and *index*. """ + _state_attrs: ClassVar = ( + _TraversalWithoutWeightsWithVisited._state_attrs + + ["depth", "event", "trace", "trace_labels", "on_trace", "index"] + ) + def __init__( self, # $$ MStrategyWithoutWeights.init_signature('TraversalDepthFirstFlex') @@ -247,7 +125,7 @@ def start_from( compute_trace: bool = False, compute_on_trace: bool = False, compute_index: bool = False, - ) -> TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]": """ # $$ insert_from('$$/method_start_from/doc_start.rst') # $$ insert_from('$$/method_start_from/doc_already_visited_std.txt') @@ -371,7 +249,7 @@ def start_from( DFSEvent.NON_TREE_EDGES | DFSEvent.SOME_NON_TREE_EDGE | DFSEvent.FORWARD_OR_CROSS_EDGE - ) and (self._is_tree or mode in DFSMode.ALL_WALKS): + ) and (self._is_tree or mode == DFSMode.ALL_WALKS): raise RuntimeError( "The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, " "and groups containing them, " @@ -450,6 +328,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # Create individual flags for events that are to be reported # (Avoids attribute resolution both for checking if an event has # to be reported and for the value that is then to report) + # MyPy: Flag cannot be compiled, so it is excluded from compilation. event_entering = DFSEvent.ENTERING_SUCCESSOR event_entering_start = DFSEvent.ENTERING_START report_leaving = DFSEvent.LEAVING_SUCCESSOR & report @@ -463,9 +342,8 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: report_forward_or_cross_edge = DFSEvent.FORWARD_OR_CROSS_EDGE & report report_none = DFSEvent.NONE # The same for the mode - mode_dfs_tree = mode & DFSMode.DFS_TREE - # mode_simple_paths = mode & DFSMode.ALL_PATHS - mode_walks = mode & DFSMode.ALL_WALKS + mode_dfs_tree = mode == DFSMode.DFS_TREE + mode_walks = mode == DFSMode.ALL_WALKS # ----- Initialize method specific bookkeeping ----- @@ -584,7 +462,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # already visited and need to check and handle this; and we have to # update the visited set. # In mode ALL_WALKS, we ignore all this - if not is_tree and mode != mode_walks: + if not is_tree and not mode_walks: event = report_none @@ -595,7 +473,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # of another vertex, or because it is given as start vertex twice), # and in mode ALL_PATHS that it already is on_trace. re_visit = False - if mode == mode_dfs_tree: + if mode_dfs_tree: """$$ MVertexSet.compile_access('v_id', '', 'set', '''\ $visited.if_contains_vertex_id_prepare_remove_and_elseadd: @@ -688,7 +566,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: continue # The vertex has not been visited before, and we are still - # in case "not is_tree and mode != mode_walks" + # in case "not is_tree and not mode_walks" if compute_on_trace: # Add to trace set, if not already done @@ -759,7 +637,7 @@ def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: # Proof for correctness of the type hole: # self._labeled_edges -> next_edges (a NextWeightedEdges) is # a NextWeightedLabeledEdges -> {edge_data_expr} is a T_labels - labels = edge_or_vertex[-1] # type: ignore[assignment] + labels = edge_or_vertex[-1] to_visit_labels_append(labels) # Store marker False: when it is reached later on, we know diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py b/tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py index a3f8503..6a63ed6 100644 --- a/tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py @@ -1,9 +1,6 @@ -from __future__ import annotations - import array -import copy import itertools -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Generator from nographs._types import ( @@ -65,6 +62,11 @@ class TraversalNeighborsThenDepthFlex( *depth*, *paths*, and *visited*. """ + _state_attrs: ClassVar = ( + _TraversalWithoutWeightsWithVisited._state_attrs + + ["depth"] + ) + def __init__( self, # $$ MStrategyWithoutWeights.init_signature('TraversalNeighborsThenDepthFlex') @@ -72,11 +74,43 @@ def __init__( "$$ MStrategyWithoutWeights.init_code(depth_computation_optional = True) $$" self._compute_depth = False # value not used, initialized during traversal + def _copy( + self, + ) -> "TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]": + """Return a copy of the current traversal. + + (We cannot use copy.copy() instead, because PyPyC has a (documented) + incompatibility in the semantics of this function. + """ + t = TraversalNeighborsThenDepthFlex( + self._vertex_to_id, self._gear, lambda v, t: [], + is_tree=self._is_tree + ) + # Attributes of Traversal + # (except for _is_tree and _vertex_to_id that we have already set) + t._labeled_edges = self._labeled_edges + t._generator = self._generator + t._start_vertices = self._start_vertices + t._build_paths = self._build_paths + t._calculation_limit = self._calculation_limit + t.paths = self.paths + t._predecessors = self._predecessors + t._attributes = self._attributes + # Attributes of _TraversalWithoutWeights + # (except for _gear that we have already set) + t._edges_with_data = self._edges_with_data + # Attributes of _TraversalWithoutWeightsWIthVisited + t.visited = self.visited + # Attributes of TraversalNeighborsThenDepthFlex + t.depth = self.depth + t._compute_depth = self._compute_depth + return t + def start_from( self, # $$ insert_from('$$/method_start_from/signature.py') compute_depth: bool = False, - ) -> TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]": """ # $$ insert_from('$$/method_start_from/doc_start.rst') # $$ insert_from('$$/method_start_from/doc_already_visited_std.txt') @@ -103,7 +137,7 @@ def _traverse(self) -> Generator[T_vertex, None, Any]: depth = -1 # The inner loop starts with incrementing, so, we pre-decrement if not compute_depth: self.depth = depth # In this case, we leave the -1 the whole time - prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + prev_traversal = self._copy() # copy of self, for keeping previous depth # vertices to expand to_expand = self._gear.sequence_of_vertices(self._start_vertices) diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py b/tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py index 6a3e659..80a4f07 100644 --- a/tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py @@ -1,8 +1,6 @@ -from __future__ import annotations - import array import itertools -from typing import Optional, Any, Generic +from typing import Optional, Any, Generic, ClassVar from collections.abc import Iterable, Generator from nographs._types import ( @@ -70,6 +68,11 @@ class TraversalTopologicalSortFlex( *depth*, *paths*, and *visited*. """ + _state_attrs: ClassVar = ( + _TraversalWithoutWeightsWithVisited._state_attrs + + ["depth", "cycle_from_start"] + ) + def __init__( self, # $$ MStrategyWithoutWeights.init_signature('TraversalTopologicalSortFlex') @@ -83,7 +86,7 @@ def __init__( def start_from( self, # $$ insert_from('$$/method_start_from/signature.py') - ) -> TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]: + ) -> "TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]": """ # $$ insert_from('$$/method_start_from/doc_start.rst') # $$ insert_from('$$/method_start_from/doc_already_visited_compatible.txt') diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py b/tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py index af25739..8310715 100644 --- a/tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py +++ b/tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py @@ -31,19 +31,19 @@ def init_signature(traversal_type: str) -> None: gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], next_vertices: Optional[ NextVertices[ - T_vertex, {traversal_type}[T_vertex, T_vertex_id, T_labels] + T_vertex, "{traversal_type}[T_vertex, T_vertex_id, T_labels]" ] ] = None, *, next_edges: Optional[ NextEdges[ - T_vertex, {traversal_type}[T_vertex, T_vertex_id, T_labels] + T_vertex, "{traversal_type}[T_vertex, T_vertex_id, T_labels]" ] ] = None, next_labeled_edges: Optional[ NextLabeledEdges[ T_vertex, - {traversal_type}[T_vertex, T_vertex_id, T_labels], + "{traversal_type}[T_vertex, T_vertex_id, T_labels]", T_labels, ] ] = None, @@ -129,6 +129,8 @@ class {class_name} ( - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) """ + _state_attrs: ClassVar = {class_name}Flex._state_attrs + def __init__( self, next_vertices: Optional[