From e846d00445aaa43496ae3733c06ccfbe3904c9d5 Mon Sep 17 00:00:00 2001 From: Kyan Machiels Date: Mon, 23 Sep 2024 15:40:56 +0200 Subject: [PATCH 1/6] Modify behaviour of get_close_matches to return multiple close matches. --- tests/test_incorrect_leaf.py | 4 ++-- wrapper.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_incorrect_leaf.py b/tests/test_incorrect_leaf.py index 6f90934..77ca654 100644 --- a/tests/test_incorrect_leaf.py +++ b/tests/test_incorrect_leaf.py @@ -5,7 +5,7 @@ def test_close_incorrect_leaf_name_recommendation_on_top_level(self): with self.assertRaises(AttributeError) as context: self.query.filter(lambda x: x.id == 247).select(lambda x: x.parent).get() - self.assertEqual(str(context.exception), "Field 'parent' not found. Did you mean 'parent_id'?") + self.assertEqual(str(context.exception), "Field 'parent' not found. Try one of the following 'parent_id,department_id,parent_user_id'") def test_very_incorrect_leaf_name_recommendation_on_top_level(self): with self.assertRaises(AttributeError) as context: @@ -17,7 +17,7 @@ def test_close_incorrect_leaf_name_recommendation_on_nested(self): with self.assertRaises(AttributeError) as context: self.query.filter(lambda x: x.id == 247).select(lambda x: x.parent_id.namm).get() - self.assertEqual(str(context.exception), "Field 'namm' not found in 'hr.employee'. Did you mean 'name'?") + self.assertEqual(str(context.exception), "Field 'namm' not found in 'hr.employee'. Try one of the following 'name'") def test_very_incorrect_leaf_name_recommendation_on_nested(self): with self.assertRaises(AttributeError) as context: diff --git a/wrapper.py b/wrapper.py index cba1ae2..69d417d 100644 --- a/wrapper.py +++ b/wrapper.py @@ -38,9 +38,9 @@ def __getattr__(self, attr: str) -> "FieldProxy": """Handle attribute access for relational fields.""" field_def = self.fields.get(self.field_name) if not field_def: - closest_match = difflib.get_close_matches(self.field_name, self.fields.keys(), n=1) - if closest_match: - raise AttributeError(f"Field '{self.field_name}' not found. Did you mean '{closest_match[0]}'?") + closest_matches = difflib.get_close_matches(self.field_name, self.fields.keys()) + if closest_matches: + raise AttributeError(f"Field '{self.field_name}' not found. Try one of the following '{','.join(closest_matches)}'") else: raise AttributeError(f"Field '{self.field_name}' not found.") @@ -55,9 +55,9 @@ def __getattr__(self, attr: str) -> "FieldProxy": # Allow 'id' and 'external_id' even if they are not in related_fields if attr not in related_fields and attr not in {"id", "external_id"}: - closest_match = difflib.get_close_matches(attr, related_fields.keys(), n=1) - if closest_match: - raise AttributeError(f"Field '{attr}' not found in '{relation}'. Did you mean '{closest_match[0]}'?") + closest_matches = difflib.get_close_matches(attr, related_fields.keys()) + if closest_matches: + raise AttributeError(f"Field '{attr}' not found in '{relation}'. Try one of the following '{','.join(closest_matches)}'") else: raise AttributeError(f"Field '{attr}' not found in '{relation}'") @@ -130,9 +130,9 @@ def __getattr__(self, item: str) -> FieldProxy: export_field_path=item, ) else: - closest_match = difflib.get_close_matches(item, self.fields.keys(), n=1) - if closest_match: - raise AttributeError(f"Field '{item}' not found. Did you mean '{closest_match[0]}'?") + closest_matches = difflib.get_close_matches(item, self.fields.keys()) + if closest_matches: + raise AttributeError(f"Field '{item}' not found. Try one of the following '{','.join(closest_matches)}'") else: raise AttributeError(f"Field '{item}' not found.") From 9e8af7764b2274f951cdf9bcceed8bafc202e41d Mon Sep 17 00:00:00 2001 From: Kyan Machiels Date: Mon, 23 Sep 2024 16:14:12 +0200 Subject: [PATCH 2/6] Prescribe AttributeError better --- wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrapper.py b/wrapper.py index 69d417d..b05dd62 100644 --- a/wrapper.py +++ b/wrapper.py @@ -47,7 +47,7 @@ def __getattr__(self, attr: str) -> "FieldProxy": if field_def.get("type") in {"many2one", "one2many", "many2many"}: relation = field_def.get("relation") if not relation: - raise AttributeError(f"No relation found for field '{self.field_name}'.") + raise AttributeError(f"'{self.field_name}' has no attributes. Remove '.{attr}'") related_fields = self.model.query.orm.fields_cache.setdefault( relation, self.model.query.orm._introspect_fields(relation) From c1ef53c1df84a53349923254a0443f5c0dd9d118 Mon Sep 17 00:00:00 2001 From: Kyan Machiels Date: Mon, 23 Sep 2024 16:22:28 +0200 Subject: [PATCH 3/6] Fix AttributeError in FieldProxy class --- wrapper.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/wrapper.py b/wrapper.py index b05dd62..5ee6646 100644 --- a/wrapper.py +++ b/wrapper.py @@ -47,7 +47,7 @@ def __getattr__(self, attr: str) -> "FieldProxy": if field_def.get("type") in {"many2one", "one2many", "many2many"}: relation = field_def.get("relation") if not relation: - raise AttributeError(f"'{self.field_name}' has no attributes. Remove '.{attr}'") + raise AttributeError(f"'{self.field_name}' has no relation. This indicates an issue with your odoo database, contact your odoo administrator.") related_fields = self.model.query.orm.fields_cache.setdefault( relation, self.model.query.orm._introspect_fields(relation) @@ -71,10 +71,7 @@ def __getattr__(self, attr: str) -> "FieldProxy": export_field_path=f"{self.export_field_path}/{attr}", ) else: - raise AttributeError( - f"Field '{self.field_name}' is not a relational field and has no attribute '{attr}'." - ) - + raise AttributeError(f"'{self.field_name}' has no attributes. Remove '.{attr}'") def __eq__(self, other: Any) -> "ModelProxy": From d0b9d7db56e290c718b19833fc4981e1b9bfae25 Mon Sep 17 00:00:00 2001 From: Kyan Machiels Date: Mon, 23 Sep 2024 16:24:31 +0200 Subject: [PATCH 4/6] Refactor comparison methods in FieldProxy class --- wrapper.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/wrapper.py b/wrapper.py index 5ee6646..7ff841d 100644 --- a/wrapper.py +++ b/wrapper.py @@ -74,35 +74,28 @@ def __getattr__(self, attr: str) -> "FieldProxy": raise AttributeError(f"'{self.field_name}' has no attributes. Remove '.{attr}'") - def __eq__(self, other: Any) -> "ModelProxy": + def __eq__(self, other: Any): self.model._register_condition((self.field_path, "=", other)) - return self.model - - def __ne__(self, other: Any) -> "ModelProxy": + + def __ne__(self, other: Any): self.model._register_condition((self.field_path, "!=", other)) - return self.model - def __contains__(self, other: Any) -> "ModelProxy": + def __contains__(self, other: Any): operator = "ilike" if isinstance(other, str) else "in" value = other if isinstance(other, str) else ([other] if not isinstance(other, list) else other) self.model._register_condition((self.field_path, operator, value)) - return self.model - def __lt__(self, other: Any) -> "ModelProxy": + def __lt__(self, other: Any): self.model._register_condition((self.field_path, "<", other)) - return self.model - def __le__(self, other: Any) -> "ModelProxy": + def __le__(self, other: Any): self.model._register_condition((self.field_path, "<=", other)) - return self.model - def __gt__(self, other: Any) -> "ModelProxy": + def __gt__(self, other: Any): self.model._register_condition((self.field_path, ">", other)) - return self.model - def __ge__(self, other: Any) -> "ModelProxy": + def __ge__(self, other: Any): self.model._register_condition((self.field_path, ">=", other)) - return self.model class ModelProxy: From 6ba43ad0c8ed579956464fb32ca5f41523380389 Mon Sep 17 00:00:00 2001 From: Kyan Machiels Date: Mon, 23 Sep 2024 16:35:29 +0200 Subject: [PATCH 5/6] Refactor field caching in ModelProxy and Client classes --- wrapper.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/wrapper.py b/wrapper.py index 7ff841d..9121ef6 100644 --- a/wrapper.py +++ b/wrapper.py @@ -49,9 +49,9 @@ def __getattr__(self, attr: str) -> "FieldProxy": if not relation: raise AttributeError(f"'{self.field_name}' has no relation. This indicates an issue with your odoo database, contact your odoo administrator.") - related_fields = self.model.query.orm.fields_cache.setdefault( - relation, self.model.query.orm._introspect_fields(relation) - ) + if relation not in self.model.query.orm.fields_cache: + self.model.query.orm.fields_cache[relation] = self.model.query.orm._introspect_fields(relation) + related_fields = self.model.query.orm.fields_cache[relation] # Allow 'id' and 'external_id' even if they are not in related_fields if attr not in related_fields and attr not in {"id", "external_id"}: @@ -99,18 +99,16 @@ def __ge__(self, other: Any): class ModelProxy: - def __init__(self, fields: Dict, query: "OdooQuery", collect_accesses: bool = False): + def __init__(self, fields: Dict, query: "OdooQuery"): self.fields = fields self.query = query self.conditions = [] self.accesses = [] - self.collect_accesses = collect_accesses def __getattr__(self, item: str) -> FieldProxy: """Handle attribute access to dynamically return a FieldProxy.""" if item in self.fields or item in {"id", "external_id"}: - if self.collect_accesses: - self.accesses.append(item) + self.accesses.append(item) return FieldProxy( field_name=item, @@ -148,7 +146,8 @@ def _authenticate(self) -> int: return self.common_proxy.authenticate(self.db, self.username, self.password, {}) def __getitem__(self, model_name: str) -> "OdooQuery": - self.fields_cache.setdefault(model_name, self._introspect_fields(model_name)) + if model_name not in self.fields_cache: + self.fields_cache[model_name] = self._introspect_fields(model_name) return OdooQuery(self, model_name) def _introspect_fields(self, model_name: str) -> Dict: @@ -193,7 +192,7 @@ def __init__(self, orm: Client, model_name: str): def select(self, select_func) -> "OdooQuery": """Apply a projection.""" - proxy = ModelProxy(self.fields, self, collect_accesses=False) + proxy = ModelProxy(self.fields, self) result = select_func(proxy) def collect_projections(res) -> List[FieldProxy]: @@ -213,14 +212,14 @@ def collect_projections(res) -> List[FieldProxy]: def filter(self, filter_func) -> "OdooQuery": """Apply filter conditions using a lambda function.""" - proxy = ModelProxy(self.fields, self, collect_accesses=True) + proxy = ModelProxy(self.fields, self) filter_func(proxy) self.filters.extend(proxy.conditions) return self def order_by(self, order_func, descending: bool = False) -> "OdooQuery": """Apply ordering on fields.""" - proxy = ModelProxy(self.fields, self, collect_accesses=False) + proxy = ModelProxy(self.fields, self) result = order_func(proxy) def collect_fields(res) -> List[FieldProxy]: From 79cadc6f59e0e4a77649df23369cbe1cb1d8a552 Mon Sep 17 00:00:00 2001 From: Kyan Machiels Date: Mon, 23 Sep 2024 16:36:37 +0200 Subject: [PATCH 6/6] Remove unused close() method in Client class --- wrapper.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/wrapper.py b/wrapper.py index 9121ef6..9b886b2 100644 --- a/wrapper.py +++ b/wrapper.py @@ -165,18 +165,7 @@ def _introspect_fields(self, model_name: str) -> Dict: def set_context(self, **kwargs): """Set the context for subsequent queries.""" self.context.update(kwargs) - - def close(self): - """Close the ServerProxy connections.""" - if self.common_proxy: - self.common_proxy("close") if hasattr(self.common_proxy, "close") else None - self.common_proxy = None - if self.object_proxy: - self.object_proxy("close") if hasattr(self.object_proxy, "close") else None - self.object_proxy = None - - def __exit__(self): - self.close() + class OdooQuery: def __init__(self, orm: Client, model_name: str):