Skip to content

Rc/5.4.3 #855

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 8 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
sonar.sources = neomodel/
sonar.tests = test/
sonar.python.version = 3.7, 3.8, 3.9, 3.10, 3.11
sonar.python.version = 3.9, 3.10, 3.11, 3.12, 3.13
6 changes: 6 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version 5.4.3 2025-02
* Add Size() scalar function
* Fix potential duplicate variable name in multiple traversals
* Minor fixes
* Note : With Neo4j now using CalVer, we can soon shift to true SemVer

Version 5.4.2 2024-12
* Add support for Neo4j Rust driver extension : pip install neomodel[rust-driver-ext]
* Add initial_context parameter to subqueries
Expand Down
2 changes: 1 addition & 1 deletion doc/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Adjust driver configuration - these options are only available for this connecti
config.MAX_TRANSACTION_RETRY_TIME = 30.0 # default
config.RESOLVER = None # default
config.TRUST = neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES # default
config.USER_AGENT = neomodel/v5.4.2 # default
config.USER_AGENT = neomodel/v5.4.3 # default

Setting the database name, if different from the default one::

Expand Down
2 changes: 1 addition & 1 deletion doc/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ Working with relationships::
len(germany.inhabitant) # 1

# Find people called 'Jim' in germany
germany.inhabitant.search(name='Jim')
germany.inhabitant.filter(name='Jim')

# Find all the people called in germany except 'Jim'
germany.inhabitant.exclude(name='Jim')
Expand Down
4 changes: 3 additions & 1 deletion doc/source/spatial_properties.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,11 @@ Working with `PointProperty`
----------------------------
To define a ``PointProperty`` Node property, simply specify it along with its ``crs``: ::

from neomodel.contrib import spatial_properties as neomodel_spatial

class SomeEntity(neomodel.StructuredNode):
entity_id = neomodel.UniqueIdProperty()
location = neomodel.PointProperty(crs='wgs-84')
location = neomodel_spatial.PointProperty(crs='wgs-84')

Given this definition of ``SomeEntity``, an object can be created by: ::

Expand Down
2 changes: 1 addition & 1 deletion neomodel/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "5.4.2"
__version__ = "5.4.3"
47 changes: 34 additions & 13 deletions neomodel/async_/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,8 @@
self._ast = QueryAST()
self._query_params: dict = {}
self._place_holder_registry: dict = {}
self._ident_count: int = 0
self._relation_identifier_count: int = 0
self._node_identifier_count: int = 0
self._subquery_namespace: TOptional[str] = subquery_namespace

async def build_ast(self) -> "AsyncQueryBuilder":
Expand Down Expand Up @@ -494,9 +495,13 @@
return await self.build_node(source)
raise ValueError("Unknown source type " + repr(source))

def create_ident(self) -> str:
self._ident_count += 1
return f"r{self._ident_count}"
def create_relation_identifier(self) -> str:
self._relation_identifier_count += 1
return f"r{self._relation_identifier_count}"

def create_node_identifier(self, prefix: str) -> str:
self._node_identifier_count += 1
return f"{prefix}{self._node_identifier_count}"

def build_order_by(self, ident: str, source: "AsyncNodeSet") -> None:
if "?" in source.order_by_elements:
Expand Down Expand Up @@ -535,7 +540,7 @@
rhs_label = ":" + traversal.target_class.__label__

# build source
rel_ident = self.create_ident()
rel_ident = self.create_relation_identifier()
lhs_ident = await self.build_source(traversal.source)
traversal_ident = f"{traversal.name}_{rel_ident}"
rhs_ident = traversal_ident + rhs_label
Expand Down Expand Up @@ -600,7 +605,7 @@
lhs_ident = stmt

already_present = part in subgraph
rel_ident = self.create_ident()
rel_ident = self.create_relation_identifier()
rhs_label = relationship.definition["node_class"].__label__
if relation.get("relation_filtering"):
rhs_name = rel_ident
Expand All @@ -610,6 +615,7 @@
rhs_name = relation["alias"]
else:
rhs_name = f"{rhs_label.lower()}_{rel_iterator}"
rhs_name = self.create_node_identifier(rhs_name)
rhs_ident = f"{rhs_name}:{rhs_label}"
if relation["include_in_return"] and not already_present:
self._additional_return(rhs_name)
Expand Down Expand Up @@ -1235,12 +1241,9 @@
class ScalarFunction(BaseFunction):
"""Base scalar function class."""

pass


@dataclass
class Last(ScalarFunction):
"""last() function."""
@property
def function_name(self) -> str:
raise NotImplementedError

Check warning on line 1246 in neomodel/async_/match.py

View check run for this annotation

Codecov / codecov/patch

neomodel/async_/match.py#L1246

Added line #L1246 was not covered by tests

def render(self, qbuilder: AsyncQueryBuilder) -> str:
if isinstance(self.input_name, str):
Expand All @@ -1250,7 +1253,25 @@
self._internal_name = self.input_name.get_internal_name()
else:
content = self.resolve_internal_name(qbuilder)
return f"last({content})"
return f"{self.function_name}({content})"


@dataclass
class Last(ScalarFunction):
"""last() function."""

@property
def function_name(self) -> str:
return "last"


@dataclass
class Size(ScalarFunction):
"""size() function."""

@property
def function_name(self) -> str:
return "size"


@dataclass
Expand Down
5 changes: 2 additions & 3 deletions neomodel/async_/relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ def __new__(
return inst


StructuredRelBase: type = RelationshipMeta(
"RelationshipBase", (AsyncPropertyManager,), {}
)
class StructuredRelBase(AsyncPropertyManager, metaclass=RelationshipMeta):
pass


class AsyncStructuredRel(StructuredRelBase):
Expand Down
10 changes: 5 additions & 5 deletions neomodel/async_/relationship_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ def __init__(
cls_name: str,
direction: int,
manager: type[AsyncRelationshipManager] = AsyncRelationshipManager,
model: Optional[AsyncStructuredRel] = None,
model: Optional[type[AsyncStructuredRel]] = None,
) -> None:
self._validate_class(cls_name, model)

Expand Down Expand Up @@ -486,7 +486,7 @@ def __init__(
adb._NODE_CLASS_REGISTRY[label_set] = model

def _validate_class(
self, cls_name: str, model: Optional[AsyncStructuredRel] = None
self, cls_name: str, model: Optional[type[AsyncStructuredRel]] = None
) -> None:
if not isinstance(cls_name, (str, object)):
raise ValueError("Expected class name or class got " + repr(cls_name))
Expand Down Expand Up @@ -552,7 +552,7 @@ def __init__(
cls_name: str,
relation_type: str,
cardinality: type[AsyncRelationshipManager] = AsyncZeroOrMore,
model: Optional[AsyncStructuredRel] = None,
model: Optional[type[AsyncStructuredRel]] = None,
) -> None:
super().__init__(
relation_type, cls_name, OUTGOING, manager=cardinality, model=model
Expand All @@ -565,7 +565,7 @@ def __init__(
cls_name: str,
relation_type: str,
cardinality: type[AsyncRelationshipManager] = AsyncZeroOrMore,
model: Optional[AsyncStructuredRel] = None,
model: Optional[type[AsyncStructuredRel]] = None,
) -> None:
super().__init__(
relation_type, cls_name, INCOMING, manager=cardinality, model=model
Expand All @@ -578,7 +578,7 @@ def __init__(
cls_name: str,
relation_type: str,
cardinality: type[AsyncRelationshipManager] = AsyncZeroOrMore,
model: Optional[AsyncStructuredRel] = None,
model: Optional[type[AsyncStructuredRel]] = None,
) -> None:
super().__init__(
relation_type, cls_name, EITHER, manager=cardinality, model=model
Expand Down
47 changes: 34 additions & 13 deletions neomodel/sync_/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,8 @@
self._ast = QueryAST()
self._query_params: dict = {}
self._place_holder_registry: dict = {}
self._ident_count: int = 0
self._relation_identifier_count: int = 0
self._node_identifier_count: int = 0
self._subquery_namespace: TOptional[str] = subquery_namespace

def build_ast(self) -> "QueryBuilder":
Expand Down Expand Up @@ -492,9 +493,13 @@
return self.build_node(source)
raise ValueError("Unknown source type " + repr(source))

def create_ident(self) -> str:
self._ident_count += 1
return f"r{self._ident_count}"
def create_relation_identifier(self) -> str:
self._relation_identifier_count += 1
return f"r{self._relation_identifier_count}"

def create_node_identifier(self, prefix: str) -> str:
self._node_identifier_count += 1
return f"{prefix}{self._node_identifier_count}"

def build_order_by(self, ident: str, source: "NodeSet") -> None:
if "?" in source.order_by_elements:
Expand Down Expand Up @@ -533,7 +538,7 @@
rhs_label = ":" + traversal.target_class.__label__

# build source
rel_ident = self.create_ident()
rel_ident = self.create_relation_identifier()
lhs_ident = self.build_source(traversal.source)
traversal_ident = f"{traversal.name}_{rel_ident}"
rhs_ident = traversal_ident + rhs_label
Expand Down Expand Up @@ -598,7 +603,7 @@
lhs_ident = stmt

already_present = part in subgraph
rel_ident = self.create_ident()
rel_ident = self.create_relation_identifier()
rhs_label = relationship.definition["node_class"].__label__
if relation.get("relation_filtering"):
rhs_name = rel_ident
Expand All @@ -608,6 +613,7 @@
rhs_name = relation["alias"]
else:
rhs_name = f"{rhs_label.lower()}_{rel_iterator}"
rhs_name = self.create_node_identifier(rhs_name)
rhs_ident = f"{rhs_name}:{rhs_label}"
if relation["include_in_return"] and not already_present:
self._additional_return(rhs_name)
Expand Down Expand Up @@ -1231,12 +1237,9 @@
class ScalarFunction(BaseFunction):
"""Base scalar function class."""

pass


@dataclass
class Last(ScalarFunction):
"""last() function."""
@property
def function_name(self) -> str:
raise NotImplementedError

Check warning on line 1242 in neomodel/sync_/match.py

View check run for this annotation

Codecov / codecov/patch

neomodel/sync_/match.py#L1242

Added line #L1242 was not covered by tests

def render(self, qbuilder: QueryBuilder) -> str:
if isinstance(self.input_name, str):
Expand All @@ -1246,7 +1249,25 @@
self._internal_name = self.input_name.get_internal_name()
else:
content = self.resolve_internal_name(qbuilder)
return f"last({content})"
return f"{self.function_name}({content})"


@dataclass
class Last(ScalarFunction):
"""last() function."""

@property
def function_name(self) -> str:
return "last"


@dataclass
class Size(ScalarFunction):
"""size() function."""

@property
def function_name(self) -> str:
return "size"


@dataclass
Expand Down
3 changes: 2 additions & 1 deletion neomodel/sync_/relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def __new__(
return inst


StructuredRelBase: type = RelationshipMeta("RelationshipBase", (PropertyManager,), {})
class StructuredRelBase(PropertyManager, metaclass=RelationshipMeta):
pass


class StructuredRel(StructuredRelBase):
Expand Down
10 changes: 5 additions & 5 deletions neomodel/sync_/relationship_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ def __init__(
cls_name: str,
direction: int,
manager: type[RelationshipManager] = RelationshipManager,
model: Optional[StructuredRel] = None,
model: Optional[type[StructuredRel]] = None,
) -> None:
self._validate_class(cls_name, model)

Expand Down Expand Up @@ -463,7 +463,7 @@ def __init__(
db._NODE_CLASS_REGISTRY[label_set] = model

def _validate_class(
self, cls_name: str, model: Optional[StructuredRel] = None
self, cls_name: str, model: Optional[type[StructuredRel]] = None
) -> None:
if not isinstance(cls_name, (str, object)):
raise ValueError("Expected class name or class got " + repr(cls_name))
Expand Down Expand Up @@ -527,7 +527,7 @@ def __init__(
cls_name: str,
relation_type: str,
cardinality: type[RelationshipManager] = ZeroOrMore,
model: Optional[StructuredRel] = None,
model: Optional[type[StructuredRel]] = None,
) -> None:
super().__init__(
relation_type, cls_name, OUTGOING, manager=cardinality, model=model
Expand All @@ -540,7 +540,7 @@ def __init__(
cls_name: str,
relation_type: str,
cardinality: type[RelationshipManager] = ZeroOrMore,
model: Optional[StructuredRel] = None,
model: Optional[type[StructuredRel]] = None,
) -> None:
super().__init__(
relation_type, cls_name, INCOMING, manager=cardinality, model=model
Expand All @@ -553,7 +553,7 @@ def __init__(
cls_name: str,
relation_type: str,
cardinality: type[RelationshipManager] = ZeroOrMore,
model: Optional[StructuredRel] = None,
model: Optional[type[StructuredRel]] = None,
) -> None:
super().__init__(
relation_type, cls_name, EITHER, manager=cardinality, model=model
Expand Down
14 changes: 9 additions & 5 deletions test/async_/test_match_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,9 @@
Optional,
RawCypher,
RelationNameResolver,
Size,
)
from neomodel.exceptions import (
FeatureNotSupported,
MultipleNodesReturned,
RelationshipClassNotDefined,
)
from neomodel.exceptions import MultipleNodesReturned, RelationshipClassNotDefined


class SupplierRel(AsyncStructuredRel):
Expand Down Expand Up @@ -778,6 +775,13 @@ async def test_annotate_and_collect():
)
assert len(result[0][1][0]) == 2 # 2 species must be there

result = (
await Supplier.nodes.traverse_relations(species="coffees__species")
.annotate(Size(Collect("species", distinct=True)))
.all()
)
assert result[0][1] == 2 # 2 species

result = (
await Supplier.nodes.traverse_relations(species="coffees__species")
.annotate(all_species=Collect("species", distinct=True))
Expand Down
Loading
Loading