# Path: formulaic/parser/types/factor.py
# class Factor:
#     """
#     Factors are the indivisable atomic unit that make up formulas.
# 
#     Each instance of `Factor` is a specification that is evaluable by a
#     materializer to generate concrete vector(s). `Factors` are multiplied
#     together into `Term`s, which in turn represent the output columns of model
#     matrices. Note that `Factor` instances are entirely abstract of data.
# 
#     Attributes:
#         expr: The (string) expression to be evaluated by the materializer.
#         eval_method: An `EvalMethod` enum instance indicating the mechanism to
#             be used to evaluate the expression (one of: unknown, literal, lookup
#             or python).
#         kind: The kind of data represented (one of: unknown, constant,
#             numerical, categorical).
#         metadata: An additional (optional) dictionary of metadata (currently
#             unused).
#     """
# 
#     class EvalMethod(Enum):
#         UNKNOWN = "unknown"
#         LITERAL = "literal"
#         LOOKUP = "lookup"
#         PYTHON = "python"
# 
#     class Kind(Enum):
#         UNKNOWN = "unknown"
#         CONSTANT = "constant"
#         NUMERICAL = "numerical"
#         CATEGORICAL = "categorical"
# 
#     __slots__ = ("expr", "_eval_method", "_kind", "metadata")
# 
#     def __init__(
#         self,
#         expr: str = "",
#         *,
#         eval_method: Optional[Union[str, EvalMethod]] = None,
#         kind: Optional[Union[str, Kind]] = None,
#         metadata: Optional[Dict] = None
#     ):
#         self.expr = expr
#         self.eval_method = eval_method
#         self.kind = kind
#         self.metadata = metadata or {}
# 
#     @property
#     def eval_method(self) -> EvalMethod:
#         return self._eval_method
# 
#     @eval_method.setter
#     def eval_method(self, eval_method):
#         self._eval_method = Factor.EvalMethod(eval_method or "unknown")
# 
#     @property
#     def kind(self) -> Kind:
#         return self._kind
# 
#     @kind.setter
#     def kind(self, kind):
#         self._kind = Factor.Kind(kind or "unknown")
# 
#     def __eq__(self, other):
#         if isinstance(other, str):
#             return self.expr == other
#         if isinstance(other, Factor):
#             return self.expr == other.expr
#         return NotImplemented
# 
#     def __hash__(self):
#         return self.expr.__hash__()
# 
#     def __lt__(self, other):
#         if isinstance(other, Factor):
#             return self.expr < other.expr
#         return NotImplemented
# 
#     def to_terms(self) -> Iterable[Term]:
#         """
#         Convert this `Factor` instance into a `Term` instance, and expose it as
#         a single-element iterable.
#         """
#         return {Term([self])}
# 
#     def __repr__(self):
#         return self.expr
# 
# Path: formulaic/parser/types/term.py
# class Term:
#     """
#     Represents a "term" of a formula.
# 
#     A "term" is a product of "factors" (represented by `Factor`) instances, and
#     a formula is made up of a sum of terms.
# 
#     Attributes:
#         factors: The set of factors to be multipled to form the term.
#     """
# 
#     def __init__(self, factors: Iterable["Factor"]):
#         self.factors = set(factors)
# 
#     def __mul__(self, other):
#         if isinstance(other, Term):
#             return Term([*self.factors, *other.factors])
#         return NotImplemented
# 
#     @property
#     def _tuple(self):
#         return tuple(factor.expr for factor in sorted(self.factors))
# 
#     def __hash__(self):
#         return hash(str(self))
# 
#     def __eq__(self, other):
#         if isinstance(other, Term):
#             return self._tuple == other._tuple
#         if isinstance(other, str):
#             return repr(self) == other
#         return NotImplemented
# 
#     def __lt__(self, other):
#         if isinstance(other, Term):
#             if len(self.factors) == len(other.factors):
#                 return sorted(self.factors) < sorted(other.factors)
#             if len(self.factors) < len(other.factors):
#                 return True
#             return False
#         return NotImplemented
# 
#     def __repr__(self):
#         return ":".join(self._tuple)


import pytest
from formulaic.parser.types import Factor, Term



class TestTerm:
    @pytest.fixture
    def term1(self):
        return Term([Factor("c"), Factor("b")])

    @pytest.fixture
    def term2(self):
        return Term([Factor("c"), Factor("d")])

    @pytest.fixture
    def term3(self):

# Path: formulaic/parser/types/factor.py
# class Factor:
#     """
#     Factors are the indivisable atomic unit that make up formulas.
# 
#     Each instance of `Factor` is a specification that is evaluable by a
#     materializer to generate concrete vector(s). `Factors` are multiplied
#     together into `Term`s, which in turn represent the output columns of model
#     matrices. Note that `Factor` instances are entirely abstract of data.
# 
#     Attributes:
#         expr: The (string) expression to be evaluated by the materializer.
#         eval_method: An `EvalMethod` enum instance indicating the mechanism to
#             be used to evaluate the expression (one of: unknown, literal, lookup
#             or python).
#         kind: The kind of data represented (one of: unknown, constant,
#             numerical, categorical).
#         metadata: An additional (optional) dictionary of metadata (currently
#             unused).
#     """
# 
#     class EvalMethod(Enum):
#         UNKNOWN = "unknown"
#         LITERAL = "literal"
#         LOOKUP = "lookup"
#         PYTHON = "python"
# 
#     class Kind(Enum):
#         UNKNOWN = "unknown"
#         CONSTANT = "constant"
#         NUMERICAL = "numerical"
#         CATEGORICAL = "categorical"
# 
#     __slots__ = ("expr", "_eval_method", "_kind", "metadata")
# 
#     def __init__(
#         self,
#         expr: str = "",
#         *,
#         eval_method: Optional[Union[str, EvalMethod]] = None,
#         kind: Optional[Union[str, Kind]] = None,
#         metadata: Optional[Dict] = None
#     ):
#         self.expr = expr
#         self.eval_method = eval_method
#         self.kind = kind
#         self.metadata = metadata or {}
# 
#     @property
#     def eval_method(self) -> EvalMethod:
#         return self._eval_method
# 
#     @eval_method.setter
#     def eval_method(self, eval_method):
#         self._eval_method = Factor.EvalMethod(eval_method or "unknown")
# 
#     @property
#     def kind(self) -> Kind:
#         return self._kind
# 
#     @kind.setter
#     def kind(self, kind):
#         self._kind = Factor.Kind(kind or "unknown")
# 
#     def __eq__(self, other):
#         if isinstance(other, str):
#             return self.expr == other
#         if isinstance(other, Factor):
#             return self.expr == other.expr
#         return NotImplemented
# 
#     def __hash__(self):
#         return self.expr.__hash__()
# 
#     def __lt__(self, other):
#         if isinstance(other, Factor):
#             return self.expr < other.expr
#         return NotImplemented
# 
#     def to_terms(self) -> Iterable[Term]:
#         """
#         Convert this `Factor` instance into a `Term` instance, and expose it as
#         a single-element iterable.
#         """
#         return {Term([self])}
# 
#     def __repr__(self):
#         return self.expr
# 
# Path: formulaic/parser/types/term.py
# class Term:
#     """
#     Represents a "term" of a formula.
# 
#     A "term" is a product of "factors" (represented by `Factor`) instances, and
#     a formula is made up of a sum of terms.
# 
#     Attributes:
#         factors: The set of factors to be multipled to form the term.
#     """
# 
#     def __init__(self, factors: Iterable["Factor"]):
#         self.factors = set(factors)
# 
#     def __mul__(self, other):
#         if isinstance(other, Term):
#             return Term([*self.factors, *other.factors])
#         return NotImplemented
# 
#     @property
#     def _tuple(self):
#         return tuple(factor.expr for factor in sorted(self.factors))
# 
#     def __hash__(self):
#         return hash(str(self))
# 
#     def __eq__(self, other):
#         if isinstance(other, Term):
#             return self._tuple == other._tuple
#         if isinstance(other, str):
#             return repr(self) == other
#         return NotImplemented
# 
#     def __lt__(self, other):
#         if isinstance(other, Term):
#             if len(self.factors) == len(other.factors):
#                 return sorted(self.factors) < sorted(other.factors)
#             if len(self.factors) < len(other.factors):
#                 return True
#             return False
#         return NotImplemented
# 
#     def __repr__(self):
#         return ":".join(self._tuple)


# Path: tests/parser/types/test_term.py
import pytest
from formulaic.parser.types import Factor, Term




class TestTerm:
    @pytest.fixture
    def term1(self):
        return Term([Factor("c"), Factor("b")])

    @pytest.fixture
    def term2(self):
        return Term([Factor("c"), Factor("d")])

    @pytest.fixture
    def term3(self):

        return Term([Factor("a"), Factor("b"), Factor("c")])