Skip to content

[WIP] Type hints #131

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 57 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
7f14e0c
Started annotating
eliotwrobson Mar 29, 2023
8794171
Update utils.py
eliotwrobson Mar 29, 2023
075934e
More generic annotations
eliotwrobson Mar 29, 2023
b6341dc
Add NFA annotations
eliotwrobson Mar 29, 2023
39314f7
More type annotations
eliotwrobson Mar 29, 2023
e9cb1c3
Mapping change
eliotwrobson Mar 29, 2023
6ac8088
Type move
eliotwrobson Mar 29, 2023
a0406a8
Update requirements.txt
eliotwrobson Mar 29, 2023
49e6da4
Changing PDA stuff
eliotwrobson Mar 29, 2023
4075e12
Remove trailing space after decorator
caleb531 Mar 31, 2023
c0a8f31
Add explicit return type annotation for copy()
caleb531 Mar 31, 2023
621b981
Fix import order to appease isort
caleb531 Mar 31, 2023
e9e081f
Fix compatibility with Python versions older than 3.11
caleb531 Mar 31, 2023
a117ee6
Solve "missing import: frozendict" error from Pylance
caleb531 Mar 31, 2023
db58ba3
Remove leftover TODO comment
caleb531 Mar 31, 2023
b822a25
Add mypy to dev dependencies for project
caleb531 Mar 31, 2023
5a0a227
Correct order of dependencies in requirements.txt
caleb531 Mar 31, 2023
fc64a3d
Format Python files on save using black
caleb531 Mar 31, 2023
1f6cc19
Remove leftover flake8-isort package
caleb531 Mar 31, 2023
c402831
Connect isort with black to sort imports on file save
caleb531 Mar 31, 2023
d138412
Add GitHub Action CI job for black
caleb531 Mar 31, 2023
8f1877d
Revert "Add GitHub Action CI job for black"
caleb531 Mar 31, 2023
123b1b4
Resurrect flake8, this time with proper black integration
caleb531 Mar 31, 2023
56fe804
Reflow comment in Automaton class to fix line length
caleb531 Mar 31, 2023
130f690
Drop Python 3.7 support
caleb531 Mar 31, 2023
dcdf664
Disable auto-formatting for now
caleb531 Mar 31, 2023
148911e
Started adding regex annotations
eliotwrobson Apr 1, 2023
b98185b
Adding more regex type hints
eliotwrobson Apr 1, 2023
d22af3f
Finished adding type annotations
eliotwrobson Apr 1, 2023
d7b37d9
Add types to abstract PDA class
caleb531 Apr 1, 2023
811d148
Format changed files with black
caleb531 Apr 1, 2023
260fcd9
Merge remote-tracking branch 'upstream/develop' into type-hints
caleb531 Apr 1, 2023
a05fef8
Fix flake8 warnings
caleb531 Apr 1, 2023
6ae4083
Fix minor drop in branch coverage due to merge
caleb531 Apr 1, 2023
46e207f
Fix ... being flagged as missing coverage
caleb531 Apr 1, 2023
410f1c4
Add types to DPDA class
caleb531 Apr 1, 2023
c96265d
Fix Python 3.8 compatibility via Tuple type
caleb531 Apr 1, 2023
8f12309
Remove rendundant type annotations
caleb531 Apr 1, 2023
ace0d2b
Add type annotations to NPDA class
caleb531 Apr 1, 2023
da62df4
Prefer Set[] type instead of set[]
caleb531 Apr 1, 2023
fe6f9c8
Add types for TM abstract base class
caleb531 Apr 2, 2023
958b31a
Add explicit return type signatures for __init__ methods
caleb531 Apr 2, 2023
0f230cb
Add type annotation for PDAStack input elements
caleb531 Apr 2, 2023
363971c
Simplify stack/configuration types using Sequence type
caleb531 Apr 2, 2023
7114701
Remove unused types from TM class
caleb531 Apr 2, 2023
2e4f7fa
Simplify PDAAcceptanceModeT type definition
caleb531 Apr 2, 2023
486680f
Ensure PDAStack stack tuple is typed to be of variable length
caleb531 Apr 2, 2023
d9452a5
Fix leftover code from 2e4f7fa changes
caleb531 Apr 2, 2023
0c6c5a1
Add type annotations for DTM, TMTape, and TMConfiguration
caleb531 Apr 2, 2023
f0e9144
Add type annotations to NTM class
caleb531 Apr 2, 2023
27c901a
Add missing type annotations to TM utilities/primitives
caleb531 Apr 2, 2023
fad817b
Add type annotations to MNTM class
caleb531 Apr 2, 2023
2cff9f8
More named tuple cleanup + rework weird function
eliotwrobson Apr 2, 2023
8c87a21
Fix flake8/black warnings in DTM class
caleb531 Apr 3, 2023
ffd0575
Add mypy type checking to CI
caleb531 Apr 3, 2023
cb6e99a
Added py.typed
eliotwrobson Apr 4, 2023
ae04a63
Remove redundant return type on subclass validate() methods
caleb531 Apr 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ exclude_lines =

pass

...

# Only check coverage for source files
include =
automata/*/*.py
52 changes: 33 additions & 19 deletions automata/base/automaton.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@
"""Classes for working with all automata, including Turing machines."""

import abc
from typing import AbstractSet, Any, Dict, Generator, Mapping, NoReturn, Tuple

from frozendict import frozendict
from typing_extensions import Self

import automata.base.config as global_config
import automata.base.exceptions as exceptions
from automata.base.utils import freeze_value

AutomatonStateT = Any
AutomatonPathT = Mapping[str, Any]
AutomatonTransitionsT = Mapping[str, AutomatonPathT]


class Automaton(metaclass=abc.ABCMeta):
"""An abstract base class for all automata, including Turing machines."""

def __init__(self, **kwargs):
__slots__: Tuple[str, ...] = tuple()

initial_state: AutomatonStateT
states: AbstractSet[AutomatonStateT]
final_states: AbstractSet[AutomatonStateT]
transitions: AutomatonTransitionsT
input_symbols: AbstractSet[str]

def __init__(self, **kwargs: Any) -> None:
if not global_config.allow_mutable_automata:
for attr_name, attr_value in kwargs.items():
object.__setattr__(self, attr_name, freeze_value(attr_value))
Expand All @@ -22,39 +36,39 @@ def __init__(self, **kwargs):
object.__setattr__(self, attr_name, attr_value)
self.__post_init__()

def __post_init__(self):
def __post_init__(self) -> None:
if global_config.should_validate_automata:
self.validate()

@abc.abstractmethod
def validate(self):
def validate(self) -> bool:
"""Return True if this automaton is internally consistent."""
raise NotImplementedError

def __setattr__(self, name, value):
def __setattr__(self, name: str, value: Any) -> NoReturn:
"""Set custom setattr to make class immutable."""
raise AttributeError(f"This {type(self).__name__} is immutable")

def __delattr__(self, name):
def __delattr__(self, name: str) -> None:
"""Set custom delattr to make class immutable."""
raise AttributeError(f"This {type(self).__name__} is immutable")

def __getstate__(self):
def __getstate__(self) -> Any:
"""Return the object's state, described by its input parameters"""
return self.input_parameters

def __setstate__(self, d):
def __setstate__(self, d: Dict[str, Any]) -> None:
"""Restore the object state from its input parameters"""
# Notice that the default __setstate__ method won't work
# because __setattr__ is disabled due to immutability
self.__init__(**d)
self.__init__(**d) # type: ignore

@abc.abstractmethod
def read_input_stepwise(self, input_str):
def read_input_stepwise(self, input_str: str) -> Generator[Any, None, None]:
"""Return a generator that yields each step while reading input."""
raise NotImplementedError

def read_input(self, input_str):
def read_input(self, input_str: str) -> AutomatonStateT:
"""
Check if the given string is accepted by this automaton.

Expand All @@ -65,29 +79,29 @@ def read_input(self, input_str):
pass
return config

def accepts_input(self, input_str):
def accepts_input(self, input_str: str) -> bool:
"""Return True if this automaton accepts the given input."""
try:
self.read_input(input_str)
return True
except exceptions.RejectionException:
return False

def _validate_initial_state(self):
def _validate_initial_state(self) -> None:
"""Raise an error if the initial state is invalid."""
if self.initial_state not in self.states:
raise exceptions.InvalidStateError(
"{} is not a valid initial state".format(self.initial_state)
)

def _validate_initial_state_transitions(self):
def _validate_initial_state_transitions(self) -> None:
"""Raise an error if the initial state has no transitions defined."""
if self.initial_state not in self.transitions and len(self.states) > 1:
raise exceptions.MissingStateError(
"initial state {} has no transitions defined".format(self.initial_state)
)

def _validate_final_states(self):
def _validate_final_states(self) -> None:
"""Raise an error if any final states are invalid."""
invalid_states = self.final_states - self.states
if invalid_states:
Expand All @@ -98,22 +112,22 @@ def _validate_final_states(self):
)

@property
def input_parameters(self):
def input_parameters(self) -> Dict[str, Any]:
"""Return the public attributes for this automaton."""
return {
attr_name: getattr(self, attr_name)
for attr_name in self.__slots__
if not attr_name.startswith("_")
}

def copy(self):
def copy(self) -> Self:
"""Create a deep copy of the automaton."""
return self.__class__(**self.input_parameters)

# Format the given value for string output via repr() or str(); this exists
# for the purpose of displaying

def _get_repr_friendly_value(self, value):
def _get_repr_friendly_value(self, value: Any) -> Any:
"""
A helper function to convert the given value / structure into a fully
mutable one by recursively processing said structure and any of its
Expand All @@ -129,14 +143,14 @@ def _get_repr_friendly_value(self, value):
else:
return value

def __repr__(self):
def __repr__(self) -> str:
"""Return a string representation of the automaton."""
values = ", ".join(
f"{attr_name}={self._get_repr_friendly_value(attr_value)!r}"
for attr_name, attr_value in self.input_parameters.items()
)
return f"{self.__class__.__qualname__}({values})"

def __contains__(self, input_str):
def __contains__(self, input_str: str) -> bool:
"""Returns whether the word is accepted by the automaton."""
return self.accepts_input(input_str)
4 changes: 2 additions & 2 deletions automata/base/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Global configuration for the library and its primitives"""


should_validate_automata = True
should_validate_automata: bool = True
# When set to True, it disables the freeze_value step
# -> You must guarantee that your code does not modify the automata
allow_mutable_automata = False
allow_mutable_automata: bool = False
37 changes: 26 additions & 11 deletions automata/base/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@
"""Miscellaneous utility functions and classes."""

from collections import defaultdict
from itertools import count
from typing import (
Any,
Callable,
DefaultDict,
Generic,
Iterable,
List,
Set,
Tuple,
TypeVar,
)

from frozendict import frozendict


def freeze_value(value):
def freeze_value(value: Any) -> Any:
"""
A helper function to convert the given value / structure into a fully
immutable one by recursively processing said structure and any of its
Expand All @@ -28,23 +40,26 @@ def freeze_value(value):
return value


def get_renaming_function(counter):
def get_renaming_function(counter: count) -> Callable[[Any], int]:
"""
A helper function that returns a renaming function to be used in the creation of
other automata. The parameter counter should be an itertools count.
This helper function will return the same distinct output taken from counter
for each distinct input.
"""

new_state_name_dict = defaultdict(lambda: next(counter))
new_state_name_dict: DefaultDict[Any, int] = defaultdict(lambda: next(counter))

def renaming_function(item):
def renaming_function(item: Any) -> int:
return new_state_name_dict[item]

return renaming_function


class PartitionRefinement:
T = TypeVar("T")


class PartitionRefinement(Generic[T]):
"""Maintain and refine a partition of a set of items into subsets.
Space usage for a partition of n items is O(n), and each refine operation
takes time proportional to the size of its argument.
Expand All @@ -53,29 +68,29 @@ class PartitionRefinement:
https://www.ics.uci.edu/~eppstein/PADS/PartitionRefinement.py
"""

__slots__ = ("_sets", "_partition")
__slots__: Tuple[str, ...] = ("_sets", "_partition")

def __init__(self, items):
def __init__(self, items: Iterable[T]) -> None:
"""Create a new partition refinement data structure for the given
items. Initially, all items belong to the same subset.
"""
S = set(items)
self._sets = {id(S): S}
self._partition = {x: id(S) for x in S}

def get_set_by_id(self, id):
def get_set_by_id(self, id: int) -> Set[T]:
"""Return the set in the partition corresponding to id."""
return self._sets[id]

def get_set_ids(self):
def get_set_ids(self) -> Iterable[int]:
"""Return set ids corresponding to the internal partition."""
return self._sets.keys()

def get_sets(self):
def get_sets(self) -> Iterable[Set[T]]:
"""Return sets corresponding to the internal partition."""
return self._sets.values()

def refine(self, S):
def refine(self, S: Iterable[T]) -> List[Tuple[int, int]]:
"""Refine each set A in the partition to the two sets
A & S, A - S. Return a list of pairs ids (id(A & S), id(A - S))
for each changed set. Within each pair, A & S will be
Expand Down
Loading