From 0ddc19bb70737e2dec0bc3deda665e8dd0201931 Mon Sep 17 00:00:00 2001 From: SmartLamScott Date: Tue, 24 Mar 2026 17:17:41 -0500 Subject: [PATCH 1/5] fixed md formatting class header as latex --- docs/filter_specifications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filter_specifications.md b/docs/filter_specifications.md index 677b319..433d3f0 100644 --- a/docs/filter_specifications.md +++ b/docs/filter_specifications.md @@ -178,7 +178,7 @@ To evaluate to **True**, either the *if* and *then* conditions must evaluate to Special variables are reserved criterion names that do not correspond to an instance variable, property, or method on the object. Instead, they return metadata about the object itself. Special variables are identified by the `$` prefix and suffix (e.g., `$CLASS$`). -#### $CLASS$ +#### \$CLASS\$ The `$CLASS$` variable returns the class name of the object as a string. It can be used as the criterion in any rule. When used, only the `==` and `!=` operators are supported. Using any other operator with `$CLASS$` will raise a FilterError. From 20a20d11cc4b65c1aa29065bc694c3f5cd7a48f6 Mon Sep 17 00:00:00 2001 From: SmartLamScott Date: Wed, 25 Mar 2026 11:28:30 -0500 Subject: [PATCH 2/5] fixed #37 --- src/object_filtering/object_filtering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/object_filtering/object_filtering.py b/src/object_filtering/object_filtering.py index aa5ed68..18d30e5 100644 --- a/src/object_filtering/object_filtering.py +++ b/src/object_filtering/object_filtering.py @@ -195,12 +195,12 @@ class GroupExpression(_LogicalExpressionBase): _valid_keys = frozenset({"logical_operator", "logical_expressions"}) logical_operator: LogicalOperator - logical_expressions: list + logical_expressions: 'list[LogicalExpression]' def __init__( self, logical_operator: LogicalOperator = "and", - logical_expressions: list = [] + logical_expressions: 'list[LogicalExpression]' = [] ) -> None: super().__init__() self["logical_operator"] = logical_operator From e87377131cb3e1be67bc56b08717aa694a80146f Mon Sep 17 00:00:00 2001 From: SmartLamScott Date: Wed, 25 Mar 2026 11:38:24 -0500 Subject: [PATCH 3/5] claude settings and instructions --- .claude/CLAUDE.md | 42 ++++++++++++++++++++++++++++++++++++++++++ .claude/settings.json | 15 +++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/settings.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..5748d7d --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,42 @@ +# Project Guidelines + +## Running Code + +Always use `uv run` to execute Python code and tests (e.g., `uv run python -m pytest`). Never use bare `python` or `py`. + +## Code Style + +### Strings + +- Use double quotes for regular strings. +- Use single quotes only for forward-reference type hint strings (e.g., `'list[LogicalExpression]'` not `"list[LogicalExpression]"`) and nested dict access inside f-strings. +- Use f-strings with double-quotes instead of string addition. + +### Formatting + +- Format Python dict literals with JSON-style pretty-printing: one key-value pair per line, 4-space indentation. Opening brace on the assignment line, each key on its own indented line, closing brace on its own line. +- Multi-line function parameters: each on its own line, indented 2 levels (8 spaces) from the `def`. + +### Docstrings + +- Use Google-style docstrings with `Args:`, `Raises:`, `Returns:` sections. +- Include type annotations in parentheses in the `Args:` section (e.g., `param (type): Description.`). + +### Imports + +- Order: stdlib, third-party, local (with blank lines between groups). + +### Type Hints + +- Use modern union syntax (`int | float`, not `Union[int, float]`). +- Always annotate return types, including `-> None`. + +### File Headers + +- `# (c) YEAR Author Name` +- `# This file is licensed under the MIT License. See LICENSE.txt for details.` + +### Tests + +- Use `unittest.TestCase` subclasses named `Test[FeatureName]`. +- One assertion focus per test method. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..ba87956 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git branch:*)", + "Bash(gh issue view:*)", + "Bash(gh pr view:*)", + "Glob", + "Grep", + "Read" + ] + } +} \ No newline at end of file From 97ad5f6c8487f23d8681c81e069596e16d7987df Mon Sep 17 00:00:00 2001 From: SmartLamScott Date: Wed, 25 Mar 2026 14:49:17 -0500 Subject: [PATCH 4/5] fixed #36 --- src/object_filtering/object_filtering.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/object_filtering/object_filtering.py b/src/object_filtering/object_filtering.py index 18d30e5..9c0154e 100644 --- a/src/object_filtering/object_filtering.py +++ b/src/object_filtering/object_filtering.py @@ -421,7 +421,9 @@ def dict_to_logical_expression(d: dict) -> LogicalExpression: elif expr_type == ObjectFilter: return ObjectFilter.from_dict(d) -def get_logical_expression_type(expression: LogicalExpression) -> type: +def get_logical_expression_type( + expression: LogicalExpression + ) -> type[bool] | type[Rule] | type[ConditionalExpression] | type[GroupExpression] | type[ObjectFilter]: """Determines the type of a LogicalExpression based on its contents. Args: From aef8308582d2baea89d28ba1ee5fd37b5cfb9412 Mon Sep 17 00:00:00 2001 From: SmartLamScott Date: Wed, 25 Mar 2026 15:13:30 -0500 Subject: [PATCH 5/5] fixed #38 --- src/object_filtering/object_filtering.py | 96 ++++++++++------ tests/test_object_filtering.py | 140 ++++++++++++++++++++++- 2 files changed, 196 insertions(+), 40 deletions(-) diff --git a/src/object_filtering/object_filtering.py b/src/object_filtering/object_filtering.py index 9c0154e..dd58634 100644 --- a/src/object_filtering/object_filtering.py +++ b/src/object_filtering/object_filtering.py @@ -88,6 +88,18 @@ def __init__( logical_expression: bool | dict = True ) -> None: super().__init__() + + if not isinstance(name, str): + raise TypeError("name must be a str.") + if not isinstance(description, str): + raise TypeError("description must be a str.") + if not isinstance(priority, int): + raise TypeError("priority must be an int.") + if not isinstance(object_types, list) or not all(isinstance(foo, str) for foo in object_types): + raise TypeError("object_types must be a list of strings.") + if not isinstance(logical_expression, LogicalExpression): + raise TypeError("logical_expression must be a LogicalExpression.") + self["name"] = name self["description"] = description self["priority"] = priority @@ -111,14 +123,7 @@ def from_dict(cls, d: dict) -> "ObjectFilter": ObjectFilter: The constructed ObjectFilter. """ _check_keys(d, cls._valid_keys, cls.__name__) - if not isinstance(d["name"], str): - raise TypeError("name must be a str.") - if not isinstance(d["description"], str): - raise TypeError("description must be a str.") - if not isinstance(d["priority"], int): - raise TypeError("priority must be an int.") - if not isinstance(d["object_types"], list): - raise TypeError("object_types must be a list.") + return cls( name=d["name"], description=d["description"], @@ -145,6 +150,22 @@ def __init__( multi_value_behavior: MultiValueBehavior = "none" ) -> None: super().__init__() + + if not isinstance(criterion, str): + raise TypeError("criterion must be a str.") + if operator not in VALID_OPERATORS: + raise ValueError( + f"operator must be one of {sorted(VALID_OPERATORS)}, " + f"got '{operator}'." + ) + if not isinstance(parameters, list): + raise TypeError("parameters must be a list.") + if multi_value_behavior not in VALID_MULTI_VALUE_BEHAVIORS: + raise ValueError( + f"multi_value_behavior must be one of {sorted(VALID_MULTI_VALUE_BEHAVIORS)}, " + f"got '{multi_value_behavior}'." + ) + self["criterion"] = criterion self["operator"] = operator self["comparison_value"] = comparison_value @@ -168,21 +189,7 @@ def from_dict(cls, d: dict) -> "Rule": Rule: The constructed Rule. """ _check_keys(d, cls._valid_keys, cls.__name__) - if not isinstance(d["criterion"], str): - raise TypeError("criterion must be a str.") - if d["operator"] not in VALID_OPERATORS: - raise ValueError( - f"operator must be one of {sorted(VALID_OPERATORS)}, " - f"got '{d['operator']}'." - ) - if not isinstance(d["parameters"], list): - raise TypeError("parameters must be a list.") - if d["multi_value_behavior"] not in VALID_MULTI_VALUE_BEHAVIORS: - raise ValueError( - f"multi_value_behavior must be one of " - f"{sorted(VALID_MULTI_VALUE_BEHAVIORS)}, " - f"got '{d['multi_value_behavior']}'." - ) + return cls( criterion=d["criterion"], operator=d["operator"], @@ -203,6 +210,15 @@ def __init__( logical_expressions: 'list[LogicalExpression]' = [] ) -> None: super().__init__() + + if logical_operator not in VALID_LOGICAL_OPERATORS: + raise ValueError( + f"logical_operator must be one of {sorted(VALID_LOGICAL_OPERATORS)}, " + f"got '{logical_operator}'." + ) + if not isinstance(logical_expressions, list) or not all(isinstance(expr, LogicalExpression) for expr in logical_expressions): + raise TypeError("logical_expressions must be a list of LogicalExpressions.") + self["logical_operator"] = logical_operator self["logical_expressions"] = logical_expressions @@ -223,14 +239,7 @@ def from_dict(cls, d: dict) -> "GroupExpression": GroupExpression: The constructed GroupExpression. """ _check_keys(d, cls._valid_keys, cls.__name__) - if d["logical_operator"] not in VALID_LOGICAL_OPERATORS: - raise ValueError( - f"logical_operator must be one of " - f"{sorted(VALID_LOGICAL_OPERATORS)}, " - f"got '{d['logical_operator']}'." - ) - if not isinstance(d["logical_expressions"], list): - raise TypeError("logical_expressions must be a list.") + return cls( logical_operator=d["logical_operator"], logical_expressions=[ @@ -249,11 +258,29 @@ class ConditionalExpression(_LogicalExpressionBase): def __init__( self, - _if: bool | dict = True, - _then: bool | dict = True, - _else: bool | dict = True + _if: 'bool | LogicalExpression' = True, + _then: 'bool | LogicalExpression' = True, + _else: 'bool | LogicalExpression' = True ) -> None: + """Creates a ConditionalExpression. + + Args: + _if (bool | LogicalExpression): The condition branch. + _then (bool | LogicalExpression): The branch evaluated when the + condition is true. + _else (bool | LogicalExpression): The branch evaluated when the + condition is false. + + Raises: + TypeError: If any branch is not a LogicalExpression. + """ super().__init__() + for name, value in (("if", _if), ("then", _then), ("else", _else)): + if not isinstance(value, (bool, _LogicalExpressionBase)): + raise TypeError( + f"'{name}' must be a bool or LogicalExpression, " + f"got {type(value).__name__}." + ) self["if"] = _if self["then"] = _then self["else"] = _else @@ -274,6 +301,7 @@ def from_dict(cls, d: dict) -> "ConditionalExpression": ConditionalExpression: The constructed ConditionalExpression. """ _check_keys(d, cls._valid_keys, cls.__name__) + return cls( _if=logical_expression_from_dict(d["if"]), _then=logical_expression_from_dict(d["then"]), diff --git a/tests/test_object_filtering.py b/tests/test_object_filtering.py index 9f48970..2dc5bf3 100644 --- a/tests/test_object_filtering.py +++ b/tests/test_object_filtering.py @@ -827,6 +827,122 @@ def test_conditional_expression_from_dict(self): assert cond["then"] is True assert cond["else"] is False + def test_rule_init_invalid_criterion(self): + with pytest.raises(TypeError): + object_filtering.Rule(criterion=123, operator="==", comparison_value=1, parameters=[], multi_value_behavior="none") + + def test_rule_init_invalid_operator(self): + with pytest.raises(ValueError): + object_filtering.Rule(criterion="x", operator="~", comparison_value=1, parameters=[], multi_value_behavior="none") + + def test_rule_init_invalid_parameters(self): + with pytest.raises(TypeError): + object_filtering.Rule(criterion="x", operator="==", comparison_value=1, parameters="not a list", multi_value_behavior="none") + + def test_rule_init_invalid_multi_value_behavior(self): + with pytest.raises(ValueError): + object_filtering.Rule(criterion="x", operator="==", comparison_value=1, parameters=[], multi_value_behavior="bad") + + def test_group_expression_init_invalid_operator(self): + with pytest.raises(ValueError): + object_filtering.GroupExpression(logical_operator="xor", logical_expressions=[True]) + + def test_group_expression_init_invalid_expressions_type(self): + with pytest.raises(TypeError): + object_filtering.GroupExpression(logical_operator="and", logical_expressions="not a list") + + def test_group_expression_init_invalid_expression_element(self): + with pytest.raises(TypeError): + object_filtering.GroupExpression(logical_operator="and", logical_expressions=[42]) + + def test_object_filter_init_invalid_name(self): + with pytest.raises(TypeError): + object_filtering.ObjectFilter(name=123, description="desc", priority=0, object_types=["obj"], logical_expression=True) + + def test_object_filter_init_invalid_description(self): + with pytest.raises(TypeError): + object_filtering.ObjectFilter(name="test", description=123, priority=0, object_types=["obj"], logical_expression=True) + + def test_object_filter_init_invalid_priority(self): + with pytest.raises(TypeError): + object_filtering.ObjectFilter(name="test", description="desc", priority="zero", object_types=["obj"], logical_expression=True) + + def test_object_filter_init_invalid_object_types_not_list(self): + with pytest.raises(TypeError): + object_filtering.ObjectFilter(name="test", description="desc", priority=0, object_types="Shape", logical_expression=True) + + def test_object_filter_init_invalid_object_types_elements(self): + with pytest.raises(TypeError): + object_filtering.ObjectFilter(name="test", description="desc", priority=0, object_types=[123], logical_expression=True) + + def test_object_filter_init_invalid_logical_expression(self): + with pytest.raises(TypeError): + object_filtering.ObjectFilter(name="test", description="desc", priority=0, object_types=["obj"], logical_expression="not valid") + + def test_object_filter_init_invalid_logical_expression_dict(self): + with pytest.raises(TypeError): + object_filtering.ObjectFilter( + name="test", + description="desc", + priority=0, + object_types=["obj"], + logical_expression={ + "criterion": "x", + "operator": "==", + "comparison_value": 1, + "parameters": [], + "multi_value_behavior": "none" + } + ) + + def test_conditional_expression_init_invalid_if(self): + with pytest.raises(TypeError): + object_filtering.ConditionalExpression(_if="not a bool", _then=True, _else=True) + + def test_conditional_expression_init_invalid_then(self): + with pytest.raises(TypeError): + object_filtering.ConditionalExpression(_if=True, _then=42, _else=True) + + def test_conditional_expression_init_invalid_else(self): + with pytest.raises(TypeError): + object_filtering.ConditionalExpression(_if=True, _then=True, _else=[1, 2]) + + def test_conditional_expression_init_invalid_if_dict(self): + with pytest.raises(TypeError): + object_filtering.ConditionalExpression( + _if={"criterion": "x", "operator": ">=", "comparison_value": 1, + "parameters": [], "multi_value_behavior": "none"}, + _then=True, + _else=True + ) + + def test_conditional_expression_init_invalid_then_dict(self): + with pytest.raises(TypeError): + object_filtering.ConditionalExpression( + _if=True, + _then={"if": True, "then": False, "else": True}, + _else=True + ) + + def test_conditional_expression_init_invalid_else_dict(self): + with pytest.raises(TypeError): + object_filtering.ConditionalExpression( + _if=True, + _then=True, + _else={"logical_operator": "and", "logical_expressions": [True]} + ) + + def test_conditional_expression_init_valid_logical_expressions(self): + rule = object_filtering.Rule("x", ">=", 1, [], "none") + group = object_filtering.GroupExpression("and", [True]) + inner_cond = object_filtering.ConditionalExpression(True, False, True) + cond = object_filtering.ConditionalExpression( + _if=rule, _then=group, _else=inner_cond + ) + assert isinstance(cond["if"], object_filtering.Rule) + assert isinstance(cond["then"], object_filtering.GroupExpression) + assert isinstance(cond["else"], object_filtering.ConditionalExpression) + def test_object_filter_from_dict(self): d = { "name": "Test Filter", @@ -836,12 +952,24 @@ def test_object_filter_from_dict(self): "logical_expression": { "logical_operator": "and", "logical_expressions": [ - {"criterion": "x", "operator": ">=", "comparison_value": 2, - "parameters": [], "multi_value_behavior": "none"}, - {"if": {"criterion": "y", "operator": ">=", - "comparison_value": 1, "parameters": [], - "multi_value_behavior": "none"}, - "then": True, "else": False} + { + "criterion": "x", + "operator": ">=", + "comparison_value": 2, + "parameters": [], + "multi_value_behavior": "none" + }, + { + "if": { + "criterion": "y", + "operator": ">=", + "comparison_value": 1, + "parameters": [], + "multi_value_behavior": "none" + }, + "then": True, + "else": False + } ] } }