From f79cbe5182ee88dbb8ae89db03de5034d779be86 Mon Sep 17 00:00:00 2001 From: Christopher Priebe <47607274+christopherpriebe@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:48:30 -0800 Subject: [PATCH] Packaging and Typing Improvements (#8) * Package Metadata Fix for Typing and Version (#6) * fix: Updated version in pyproject.toml * feat: Added py.typed to enable typing stubs recognition. * fix: Dynamic version works with packaging. * fix: Package version now correct. * Upgrade to Strict Type Checking (#7) * fix: Moved to strict type checking and fixed all type errors. --- pyproject.toml | 5 ++++ src/fhy_core/constraint.py | 7 ++++++ src/fhy_core/expression/__init__.py | 20 ++++++++++++++++ src/fhy_core/expression/core.py | 18 +++++++++++++++ src/fhy_core/expression/parser.py | 16 +++++++------ src/fhy_core/expression/passes/__init__.py | 9 ++++++++ src/fhy_core/expression/passes/basic.py | 7 +++++- src/fhy_core/expression/passes/sympy.py | 7 ++++++ src/fhy_core/expression/pprint.py | 10 ++++++++ src/fhy_core/expression/visitor.py | 17 ++++++++++++++ src/fhy_core/identifier.py | 2 ++ src/fhy_core/param/__init__.py | 10 ++++++++ src/fhy_core/param/core.py | 27 +++++++++++++++------- src/fhy_core/param/fundamental.py | 2 ++ src/fhy_core/py.typed | 0 src/fhy_core/utils/__init__.py | 8 +++++++ src/fhy_core/utils/dict_utils.py | 2 ++ src/fhy_core/utils/lattice.py | 4 +++- src/fhy_core/utils/poset.py | 23 +++++++++++++----- src/fhy_core/utils/stack.py | 4 +++- 20 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 src/fhy_core/py.typed diff --git a/pyproject.toml b/pyproject.toml index 6df26bf..1fc796b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ Issues = "https://github.com/actlab-fhy/FhY-core/issues" [tool.setuptools.dynamic] readme = {file = ["README.md"], content-type = "text/markdown"} dependencies = {file = ["requirements.txt"]} +version = {attr = "fhy_core.__version__"} [tool.setuptools] package-dir = {"" = "src"} @@ -41,6 +42,9 @@ package-dir = {"" = "src"} where = ["src"] exclude = ["tests"] +[tool.setuptools.package-data] +fhy_core = ["py.typed"] + [project.optional-dependencies] dev = ["fhy_core[test,lint,type,docs]", "tox"] test = ["pytest", "coverage", "pytest-xdist"] @@ -73,6 +77,7 @@ exclude_also = [ [tool.mypy] pretty = false +strict = true [tool.pylint.main] extension-pkg-whitelist = ["networkx"] diff --git a/src/fhy_core/constraint.py b/src/fhy_core/constraint.py index 8564b70..98f6f92 100644 --- a/src/fhy_core/constraint.py +++ b/src/fhy_core/constraint.py @@ -1,5 +1,12 @@ """Constraint utility.""" +__all__ = [ + "Constraint", + "EquationConstraint", + "InSetConstraint", + "NotInSetConstraint", +] + from abc import ABC, abstractmethod from typing import Any diff --git a/src/fhy_core/expression/__init__.py b/src/fhy_core/expression/__init__.py index c2fcef2..11c4ca0 100644 --- a/src/fhy_core/expression/__init__.py +++ b/src/fhy_core/expression/__init__.py @@ -1,5 +1,25 @@ """General expression tree utility.""" +__all__ = [ + "BinaryExpression", + "BinaryOperation", + "Expression", + "IdentifierExpression", + "LiteralExpression", + "LiteralType", + "UnaryExpression", + "UnaryOperation", + "collect_identifiers", + "convert_expression_to_sympy_expression", + "convert_sympy_expression_to_expression", + "copy_expression", + "parse_expression", + "pformat_expression", + "simplify_expression", + "substitute_sympy_expression_variables", + "tokenize_expression", +] + from .core import ( BinaryExpression, BinaryOperation, diff --git a/src/fhy_core/expression/core.py b/src/fhy_core/expression/core.py index bd2f29d..c4ff13c 100644 --- a/src/fhy_core/expression/core.py +++ b/src/fhy_core/expression/core.py @@ -1,5 +1,23 @@ """General expression tree.""" +__all__ = [ + "Expression", + "UnaryOperation", + "UNARY_OPERATION_FUNCTION_NAMES", + "UNARY_FUNCTION_NAME_OPERATIONS", + "UNARY_OPERATION_SYMBOLS", + "UNARY_SYMBOL_OPERATIONS", + "UnaryExpression", + "BinaryOperation", + "BINARY_OPERATION_FUNCTION_NAMES", + "BINARY_FUNCTION_NAME_OPERATIONS", + "BINARY_OPERATION_SYMBOLS", + "BINARY_SYMBOL_OPERATIONS", + "BinaryExpression", + "IdentifierExpression", + "LiteralExpression", +] + from abc import ABC from dataclasses import dataclass from enum import Enum, auto diff --git a/src/fhy_core/expression/parser.py b/src/fhy_core/expression/parser.py index cef0389..0f5b5ba 100644 --- a/src/fhy_core/expression/parser.py +++ b/src/fhy_core/expression/parser.py @@ -1,5 +1,7 @@ """Parser from strings to expression trees.""" +__all__ = ["parse_expression", "tokenize_expression"] + import re from typing import Callable, TypeVar @@ -126,23 +128,23 @@ def _equality(self) -> Expression: def _comparison(self) -> Expression: return self._binary_operation(_COMPARISON_SYMBOL_OPERATIONS, self._addition) - def _addition(self): - return self._binary_operation(_ADDITION_SYMBOL_OPERATIONS, self.multiplication) + def _addition(self) -> Expression: + return self._binary_operation(_ADDITION_SYMBOL_OPERATIONS, self._multiplication) - def multiplication(self): + def _multiplication(self) -> Expression: return self._binary_operation(_MULTIPLICATION_SYMBOL_OPERATIONS, self._unary) - def _unary(self): + def _unary(self) -> Expression: if self._match("-", "+", "!"): op = self._get_previous_token() right = self._unary() return UnaryExpression(UNARY_SYMBOL_OPERATIONS[op], right) return self._exponentiation() - def _exponentiation(self): + def _exponentiation(self) -> Expression: return self._binary_operation(_EXPONENTIATION_SYMBOL_OPERATIONS, self._primary) - def _primary(self): + def _primary(self) -> Expression: if self._match_number(): return LiteralExpression(self._get_previous_token()) elif self._match_identifier(): @@ -193,7 +195,7 @@ def _match_identifier(self) -> bool: return True return False - def _consume_token(self, token: str, error_message: str): + def _consume_token(self, token: str, error_message: str) -> str: if self._peek_at_current_token() == token: return self._advance_to_next_token() else: diff --git a/src/fhy_core/expression/passes/__init__.py b/src/fhy_core/expression/passes/__init__.py index 931acf1..e6d5403 100644 --- a/src/fhy_core/expression/passes/__init__.py +++ b/src/fhy_core/expression/passes/__init__.py @@ -1,5 +1,14 @@ """Analysis and transformation functions for expressions.""" +__all__ = [ + "collect_identifiers", + "copy_expression", + "convert_expression_to_sympy_expression", + "convert_sympy_expression_to_expression", + "simplify_expression", + "substitute_sympy_expression_variables", +] + from .basic import collect_identifiers, copy_expression from .sympy import ( convert_expression_to_sympy_expression, diff --git a/src/fhy_core/expression/passes/basic.py b/src/fhy_core/expression/passes/basic.py index 0c6a291..1af72d2 100644 --- a/src/fhy_core/expression/passes/basic.py +++ b/src/fhy_core/expression/passes/basic.py @@ -1,5 +1,10 @@ """Basic expression passes.""" +__all__ = [ + "collect_identifiers", + "copy_expression", +] + from fhy_core.expression.core import ( Expression, IdentifierExpression, @@ -13,7 +18,7 @@ class IdentifierCollector(ExpressionVisitor): _identifiers: set[Identifier] - def __init__(self): + def __init__(self) -> None: self._identifiers = set() @property diff --git a/src/fhy_core/expression/passes/sympy.py b/src/fhy_core/expression/passes/sympy.py index 60da0a4..3f4191a 100644 --- a/src/fhy_core/expression/passes/sympy.py +++ b/src/fhy_core/expression/passes/sympy.py @@ -1,5 +1,12 @@ """Expression passes that interface with SymPy.""" +__all__ = [ + "convert_expression_to_sympy_expression", + "convert_sympy_expression_to_expression", + "simplify_expression", + "substitute_sympy_expression_variables", +] + import operator from typing import Any, Callable diff --git a/src/fhy_core/expression/pprint.py b/src/fhy_core/expression/pprint.py index 3b1c89b..322d8aa 100644 --- a/src/fhy_core/expression/pprint.py +++ b/src/fhy_core/expression/pprint.py @@ -1,5 +1,7 @@ """Pretty-printer for expressions.""" +__all__ = ["pformat_expression"] + from .core import ( BINARY_OPERATION_FUNCTION_NAMES, BINARY_OPERATION_SYMBOLS, @@ -27,6 +29,14 @@ def __init__( self._is_id_shown = is_id_shown self._is_printed_functional = is_printed_functional + def __call__(self, expression: Expression) -> str: + formatted_expression = super().__call__(expression) + if not isinstance(formatted_expression, str): + raise TypeError( + f"Invalid formatted expression type: {type(formatted_expression)}" + ) + return formatted_expression + def visit_unary_expression(self, unary_expression: UnaryExpression) -> str: if self._is_printed_functional: return ( diff --git a/src/fhy_core/expression/visitor.py b/src/fhy_core/expression/visitor.py index 70627b0..e03dd6b 100644 --- a/src/fhy_core/expression/visitor.py +++ b/src/fhy_core/expression/visitor.py @@ -1,5 +1,11 @@ """Expression tree visitor and transformer.""" +__all__ = [ + "ExpressionBasePass", + "ExpressionVisitor", + "ExpressionTransformer", +] + from abc import ABC from typing import Any @@ -94,6 +100,9 @@ def visit_literal_expression(self, literal_expression: LiteralExpression) -> Any class ExpressionVisitor(ExpressionBasePass, ABC): """Visitor for expression trees.""" + def __call__(self, expression: Expression) -> None: + super().__call__(expression) + def visit_unary_expression(self, unary_expression: UnaryExpression) -> None: self.visit(unary_expression.operand) @@ -113,6 +122,14 @@ def visit_literal_expression( class ExpressionTransformer(ExpressionBasePass, ABC): """Transformer for expression trees.""" + def __call__(self, expression: Expression) -> Expression: + transformed_expression = super().__call__(expression) + if not isinstance(transformed_expression, Expression): + raise TypeError( + f"Invalid transformed expression type: {type(transformed_expression)}" + ) + return transformed_expression + def visit_unary_expression(self, unary_expression: UnaryExpression) -> Expression: new_expression = self.visit(unary_expression.operand) return UnaryExpression( diff --git a/src/fhy_core/identifier.py b/src/fhy_core/identifier.py index 636ea2d..08e7083 100644 --- a/src/fhy_core/identifier.py +++ b/src/fhy_core/identifier.py @@ -1,5 +1,7 @@ """Unique identifier for named FhY objects.""" +__all__ = ["Identifier"] + from typing import Any diff --git a/src/fhy_core/param/__init__.py b/src/fhy_core/param/__init__.py index cff5a62..ed1e583 100644 --- a/src/fhy_core/param/__init__.py +++ b/src/fhy_core/param/__init__.py @@ -1,4 +1,14 @@ """Constrained parameters.""" +__all__ = [ + "CategoricalParam", + "IntParam", + "OrdinalParam", + "Param", + "PermParam", + "RealParam", + "NatParam", +] + from .core import CategoricalParam, IntParam, OrdinalParam, Param, PermParam, RealParam from .fundamental import NatParam diff --git a/src/fhy_core/param/core.py b/src/fhy_core/param/core.py index 6338385..aacf1b9 100644 --- a/src/fhy_core/param/core.py +++ b/src/fhy_core/param/core.py @@ -1,7 +1,16 @@ """Core parameter structures.""" +__all__ = [ + "Param", + "RealParam", + "IntParam", + "OrdinalParam", + "CategoricalParam", + "PermParam", +] + from abc import ABC -from collections.abc import Sequence +from collections.abc import Hashable, Sequence from typing import Any, Generic, TypeVar from fhy_core.constraint import ( @@ -12,8 +21,10 @@ from fhy_core.expression import IdentifierExpression, LiteralExpression from fhy_core.identifier import Identifier +H = TypeVar("H", bound=Hashable) + -def _is_values_unique_in_sequence_without_set(values: Sequence) -> bool: +def _is_values_unique_in_sequence_without_set(values: Sequence[Any]) -> bool: for i, value_1 in enumerate(values): for value_2 in values[i + 1 :]: if value_1 == value_2: @@ -21,11 +32,11 @@ def _is_values_unique_in_sequence_without_set(values: Sequence) -> bool: return True -def _is_values_unique_in_sorted_sequence(values: Sequence) -> bool: +def _is_values_unique_in_sorted_sequence(values: Sequence[Any]) -> bool: return all(values[i] != values[i + 1] for i in range(len(values) - 1)) -def _is_values_unique_in_sequence_with_set(values: Sequence) -> bool: +def _is_values_unique_in_sequence_with_set(values: Sequence[H]) -> bool: return len(values) == len(set(values)) @@ -161,7 +172,7 @@ def add_constraint(self, constraint: Constraint) -> None: return super().add_constraint(constraint) -class CategoricalParam(Param[Any]): +class CategoricalParam(Param[H]): """Categorical parameter. Note: @@ -169,15 +180,15 @@ class CategoricalParam(Param[Any]): """ - _categories: set[Any] + _categories: set[H] - def __init__(self, categories: Sequence[Any], name: Identifier | None = None): + def __init__(self, categories: Sequence[H], name: Identifier | None = None): super().__init__(name) if not _is_values_unique_in_sequence_with_set(categories): raise ValueError("Values must be unique.") self._categories = set(categories) - def set_value(self, value: Any) -> None: + def set_value(self, value: H) -> None: if value not in self._categories: raise ValueError("Value is not in the set of allowed categories.") return super().set_value(value) diff --git a/src/fhy_core/param/fundamental.py b/src/fhy_core/param/fundamental.py index 9d10185..c2b9c39 100644 --- a/src/fhy_core/param/fundamental.py +++ b/src/fhy_core/param/fundamental.py @@ -1,5 +1,7 @@ """Fundamental parameter classes.""" +__all__ = ["NatParam"] + from fhy_core.constraint import EquationConstraint from fhy_core.expression import ( BinaryExpression, diff --git a/src/fhy_core/py.typed b/src/fhy_core/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/fhy_core/utils/__init__.py b/src/fhy_core/utils/__init__.py index fbb3729..e3d69c1 100644 --- a/src/fhy_core/utils/__init__.py +++ b/src/fhy_core/utils/__init__.py @@ -1,5 +1,13 @@ """General utilities.""" +__all__ = [ + "invert_dict", + "invert_frozen_dict", + "Lattice", + "PartiallyOrderedSet", + "Stack", +] + from .dict_utils import invert_dict, invert_frozen_dict from .lattice import Lattice from .poset import PartiallyOrderedSet diff --git a/src/fhy_core/utils/dict_utils.py b/src/fhy_core/utils/dict_utils.py index 05e53d0..0e65ebd 100644 --- a/src/fhy_core/utils/dict_utils.py +++ b/src/fhy_core/utils/dict_utils.py @@ -1,5 +1,7 @@ """Dictionary manipulation utilities.""" +__all__ = ["invert_dict", "invert_frozen_dict"] + from typing import TypeVar from frozendict import frozendict diff --git a/src/fhy_core/utils/lattice.py b/src/fhy_core/utils/lattice.py index 6c56783..2dc58c8 100644 --- a/src/fhy_core/utils/lattice.py +++ b/src/fhy_core/utils/lattice.py @@ -1,5 +1,7 @@ """Lattice (order theory) utility.""" +__all__ = ["Lattice"] + from typing import Generic, TypeVar from .poset import PartiallyOrderedSet @@ -12,7 +14,7 @@ class Lattice(Generic[T]): _poset: PartiallyOrderedSet[T] - def __init__(self): + def __init__(self) -> None: self._poset = PartiallyOrderedSet[T]() def __contains__(self, element: T) -> bool: diff --git a/src/fhy_core/utils/poset.py b/src/fhy_core/utils/poset.py index 03f0212..076cb92 100644 --- a/src/fhy_core/utils/poset.py +++ b/src/fhy_core/utils/poset.py @@ -1,6 +1,8 @@ """Partially ordered set (poset) utility.""" -from typing import Generic, TypeVar +__all__ = ["PartiallyOrderedSet"] + +from typing import Generic, Iterator, TypeVar import networkx as nx # type: ignore @@ -12,13 +14,16 @@ class PartiallyOrderedSet(Generic[T]): _graph: nx.DiGraph - def __init__(self): + def __init__(self) -> None: self._graph = nx.DiGraph() def __contains__(self, element: T) -> bool: - return self._graph.has_node(element) + is_contain = self._graph.has_node(element) + if not isinstance(is_contain, bool): + raise TypeError(f"Expected bool, but got {type(is_contain)}") + return is_contain - def __iter__(self): + def __iter__(self) -> Iterator[T]: return iter(nx.topological_sort(self._graph)) def __len__(self) -> int: @@ -73,7 +78,10 @@ def is_less_than(self, lower: T, upper: T) -> bool: """ self._check_element_in_poset(lower) self._check_element_in_poset(upper) - return nx.has_path(self._graph, lower, upper) + has_path = nx.has_path(self._graph, lower, upper) + if not isinstance(has_path, bool): + raise TypeError(f"Expected bool, but got {type(has_path)}") + return has_path def is_greater_than(self, lower: T, upper: T) -> bool: """Check if one element is greater than another. @@ -91,7 +99,10 @@ def is_greater_than(self, lower: T, upper: T) -> bool: """ self._check_element_in_poset(lower) self._check_element_in_poset(upper) - return nx.has_path(self._graph, upper, lower) + has_path = nx.has_path(self._graph, upper, lower) + if not isinstance(has_path, bool): + raise TypeError(f"Expected bool, but got {type(has_path)}") + return has_path def _check_element_not_in_poset(self, element: T) -> None: if element in self: diff --git a/src/fhy_core/utils/stack.py b/src/fhy_core/utils/stack.py index a0c2f72..fcbb737 100644 --- a/src/fhy_core/utils/stack.py +++ b/src/fhy_core/utils/stack.py @@ -1,5 +1,7 @@ """Stack utility.""" +__all__ = ["Stack"] + from collections import deque from collections.abc import Iterator from typing import Generic, TypeVar @@ -40,7 +42,7 @@ class Stack(Generic[T]): _stack: deque[T] _iter_index: int - def __init__(self): + def __init__(self) -> None: self._stack = deque[T]() self._iter_index = 0