From c7f03eda341f5abee3ee19fc6990bb97b5cecdf0 Mon Sep 17 00:00:00 2001 From: abates20 <131566916+abates20@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:51:36 -0500 Subject: [PATCH 1/5] fix: Updated DictList type hinting to use TypeVar to allow specification of object type. --- src/cobra/core/dictlist.py | 64 ++++++++++++++++++++------------------ src/cobra/core/model.py | 8 ++--- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/cobra/core/dictlist.py b/src/cobra/core/dictlist.py index b4a5d0468..c2366307b 100644 --- a/src/cobra/core/dictlist.py +++ b/src/cobra/core/dictlist.py @@ -12,13 +12,15 @@ Pattern, Tuple, Type, + TypeVar, Union, ) -from .object import Object +_TObject = TypeVar("_TObject") -class DictList(list): + +class DictList(List[_TObject]): """ Define a combined dict and list. @@ -49,12 +51,12 @@ def __init__(self, *args): self.extend(other) # noinspection PyShadowingBuiltins - def has_id(self, id: Union[Object, str]) -> bool: + def has_id(self, id: Union[_TObject, str]) -> bool: """Check if id is in DictList.""" return id in self._dict # noinspection PyShadowingBuiltins - def _check(self, id: Union[Object, str]) -> None: + def _check(self, id: Union[_TObject, str]) -> None: """Make sure duplicate id's are not added. This function is called before adding in elements. @@ -68,7 +70,7 @@ def _generate_index(self) -> None: self._dict = {v.id: k for k, v in enumerate(self)} # noinspection PyShadowingBuiltins - def get_by_id(self, id: Union[Object, str]) -> Object: + def get_by_id(self, id: Union[_TObject, str]) -> _TObject: """Return the element with a matching id.""" return list.__getitem__(self, self._dict[id]) @@ -76,7 +78,7 @@ def list_attr(self, attribute: str) -> list: """Return a list of the given attribute for every object.""" return [getattr(i, attribute) for i in self] - def get_by_any(self, iterable: List[Union[str, Object, int]]) -> list: + def get_by_any(self, iterable: List[Union[str, _TObject, int]]) -> List[_TObject]: """Get a list of members using several different ways of indexing. Parameters @@ -92,7 +94,7 @@ def get_by_any(self, iterable: List[Union[str, Object, int]]) -> list: a list of members """ - def get_item(item: Any) -> Any: + def get_item(item: Any) -> _TObject: if isinstance(item, int): return self[item] elif isinstance(item, str): @@ -110,7 +112,7 @@ def query( self, search_function: Union[str, Pattern, Callable], attribute: Union[str, None] = None, - ) -> "DictList": + ) -> "DictList[_TObject]": """Query the list. Parameters @@ -167,21 +169,21 @@ def select_attribute(x: Optional[Any]) -> Any: results._extend_nocheck(matches) return results - def _replace_on_id(self, new_object: Object) -> None: + def _replace_on_id(self, new_object: _TObject) -> None: """Replace an object by another with the same id.""" the_id = new_object.id the_index = self._dict[the_id] list.__setitem__(self, the_index, new_object) # overriding default list functions with new ones - def append(self, entity: Object) -> None: + def append(self, entity: _TObject) -> None: """Append object to end.""" the_id = entity.id self._check(the_id) self._dict[the_id] = len(self) list.append(self, entity) - def union(self, iterable: Iterable[Object]) -> None: + def union(self, iterable: Iterable[_TObject]) -> None: """Add elements with id's not already in the model.""" _dict = self._dict append = self.append @@ -189,7 +191,7 @@ def union(self, iterable: Iterable[Object]) -> None: if i.id not in _dict: append(i) - def extend(self, iterable: Iterable[Object]) -> None: + def extend(self, iterable: Iterable[_TObject]) -> None: """Extend list by appending elements from the iterable. Sometimes during initialization from an older pickle, _dict @@ -222,7 +224,7 @@ def extend(self, iterable: Iterable[Object]) -> None: f"Is it present twice?" ) - def _extend_nocheck(self, iterable: Iterable[Object]) -> None: + def _extend_nocheck(self, iterable: Iterable[_TObject]) -> None: """Extend without checking for uniqueness. This function should only be used internally by DictList when it @@ -244,7 +246,7 @@ def _extend_nocheck(self, iterable: Iterable[Object]) -> None: for i, obj in enumerate(islice(self, current_length, None), current_length): _dict[obj.id] = i - def __sub__(self, other: Iterable[Object]) -> "DictList": + def __sub__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": """Remove a value or values, and returns the new DictList. x.__sub__(y) <==> x - y @@ -264,7 +266,7 @@ def __sub__(self, other: Iterable[Object]) -> "DictList": total.remove(item) return total - def __isub__(self, other: Iterable[Object]) -> "DictList": + def __isub__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": """Remove a value or values in place. x.__sub__(y) <==> x -= y @@ -278,7 +280,7 @@ def __isub__(self, other: Iterable[Object]) -> "DictList": self.remove(item) return self - def __add__(self, other: Iterable[Object]) -> "DictList": + def __add__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": """Add item while returning a new DictList. x.__add__(y) <==> x + y @@ -294,7 +296,7 @@ def __add__(self, other: Iterable[Object]) -> "DictList": total.extend(other) return total - def __iadd__(self, other: Iterable[Object]) -> "DictList": + def __iadd__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": """Add item while returning the same DictList. x.__iadd__(y) <==> x += y @@ -309,7 +311,7 @@ def __iadd__(self, other: Iterable[Object]) -> "DictList": self.extend(other) return self - def __reduce__(self) -> Tuple[Type["DictList"], Tuple, dict, Iterator]: + def __reduce__(self) -> Tuple[Type["DictList"], Tuple, dict, Iterator[_TObject]]: """Return a reduced version of DictList. This reduced version details the class, an empty Tuple, a dictionary of the @@ -336,7 +338,7 @@ def __setstate__(self, state: dict) -> None: self._generate_index() # noinspection PyShadowingBuiltins - def index(self, id: Union[str, Object], *args) -> int: + def index(self, id: Union[str, _TObject], *args) -> int: """Determine the position in the list. Parameters @@ -360,7 +362,7 @@ def index(self, id: Union[str, Object], *args) -> int: except KeyError: raise ValueError(f"{str(id)} not found") - def __contains__(self, entity: Union[str, Object]) -> bool: + def __contains__(self, entity: Union[str, _TObject]) -> bool: """Ask if the DictList contain an entity. DictList.__contains__(entity) <==> entity in DictList @@ -377,14 +379,14 @@ def __contains__(self, entity: Union[str, Object]) -> bool: the_id = entity return the_id in self._dict - def __copy__(self) -> "DictList": + def __copy__(self) -> "DictList[_TObject]": """Copy the DictList into a new one.""" the_copy = DictList() list.extend(the_copy, self) the_copy._dict = self._dict.copy() return the_copy - def insert(self, index: int, entity: Object) -> None: + def insert(self, index: int, entity: _TObject) -> None: """Insert entity before index.""" self._check(entity.id) list.insert(self, index, entity) @@ -395,7 +397,7 @@ def insert(self, index: int, entity: Object) -> None: _dict[i] = j + 1 _dict[entity.id] = index - def pop(self, *args) -> Object: + def pop(self, *args) -> _TObject: """Remove and return item at index (default last).""" value = list.pop(self, *args) index = self._dict.pop(value.id) @@ -409,11 +411,11 @@ def pop(self, *args) -> Object: _dict[i] = j - 1 return value - def add(self, x: Object) -> None: + def add(self, x: _TObject) -> None: """Opposite of `remove`. Mirrors set.add.""" self.extend([x]) - def remove(self, x: Union[str, Object]) -> None: + def remove(self, x: Union[str, _TObject]) -> None: """.. warning :: Internal use only. Each item is unique in the list which allows this @@ -445,8 +447,8 @@ def key(i): self._generate_index() def __getitem__( - self, i: Union[int, slice, Iterable, Object, "DictList"] - ) -> Union["DictList", Object]: + self, i: Union[int, slice, Iterable, _TObject, "DictList[_TObject]"] + ) -> Union["DictList[_TObject]", _TObject]: """Get item from DictList.""" if isinstance(i, int): return list.__getitem__(self, i) @@ -465,7 +467,9 @@ def __getitem__( else: return list.__getitem__(self, i) - def __setitem__(self, i: Union[slice, int], y: Union[list, Object]) -> None: + def __setitem__( + self, i: Union[slice, int], y: Union[List[_TObject], _TObject] + ) -> None: """Set an item via index or slice. Parameters @@ -507,11 +511,11 @@ def __delitem__(self, index: int) -> None: if j > index: _dict[i] = j - 1 - def __getslice__(self, i: int, j: int) -> "DictList": + def __getslice__(self, i: int, j: int) -> "DictList[_TObject]": """Get a slice from it to j of DictList.""" return self.__getitem__(slice(i, j)) - def __setslice__(self, i: int, j: int, y: Union[list, Object]) -> None: + def __setslice__(self, i: int, j: int, y: Union[List[_TObject], _TObject]) -> None: """Set slice, where y is an iterable.""" self.__setitem__(slice(i, j), y) diff --git a/src/cobra/core/model.py b/src/cobra/core/model.py index 2d1215a12..8c2d26429 100644 --- a/src/cobra/core/model.py +++ b/src/cobra/core/model.py @@ -81,10 +81,10 @@ def __init__( self._solver = id_or_model.solver else: Object.__init__(self, id_or_model, name=name) - self.genes = DictList() - self.reactions = DictList() # A list of cobra.Reactions - self.metabolites = DictList() # A list of cobra.Metabolites - self.groups = DictList() # A list of cobra.Groups + self.genes: DictList[Gene] = DictList() + self.reactions: DictList[Reaction] = DictList() + self.metabolites: DictList[Metabolite] = DictList() + self.groups: DictList[Group] = DictList() # genes based on their ids {Gene.id: Gene} self._compartments = {} self._contexts = [] From c9ffcf98f5919d916a355d1e39c548493815943a Mon Sep 17 00:00:00 2001 From: abates20 <131566916+abates20@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:21:29 -0500 Subject: [PATCH 2/5] fix: Add Object bound to TypeVar for DictList. --- src/cobra/core/dictlist.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cobra/core/dictlist.py b/src/cobra/core/dictlist.py index c2366307b..852f74d3c 100644 --- a/src/cobra/core/dictlist.py +++ b/src/cobra/core/dictlist.py @@ -16,8 +16,10 @@ Union, ) +from .object import Object -_TObject = TypeVar("_TObject") + +_TObject = TypeVar("_TObject", bound=Object) class DictList(List[_TObject]): From b3d68f5b532f4e14874e9b517ea7eb75bde53972 Mon Sep 17 00:00:00 2001 From: abates20 <131566916+abates20@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:36:22 -0500 Subject: [PATCH 3/5] chore: Added release notes entry for updated DictList type hinting. --- release-notes/next-release.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-notes/next-release.md b/release-notes/next-release.md index 9c2caf45a..c43f45fe8 100644 --- a/release-notes/next-release.md +++ b/release-notes/next-release.md @@ -11,6 +11,8 @@ ## Other +- Updated type hinting for the `DictList` class so that the type of `Object` contained by a `DictList` can be specified. For example, the hinted return type of `model.reactions.get_by_id` is now `Reaction` instead of `Object`. + ## Deprecated features - Changed the type of the `loopless` parameter in `flux_variability_analysis` from `bool` to `Optional[str]`. Using `loopless=False` or `loopless=True` (boolean) is now deprecated. From 3352c767d1d10ae8cbc5e991845d2dabc4ce9980 Mon Sep 17 00:00:00 2001 From: abates20 <131566916+abates20@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:56:37 -0500 Subject: [PATCH 4/5] fix: Change return type of DictList.__getattr__ to _TObject. --- src/cobra/core/dictlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cobra/core/dictlist.py b/src/cobra/core/dictlist.py index 852f74d3c..77c602207 100644 --- a/src/cobra/core/dictlist.py +++ b/src/cobra/core/dictlist.py @@ -525,7 +525,7 @@ def __delslice__(self, i: int, j: int) -> None: """Remove slice.""" self.__delitem__(slice(i, j)) - def __getattr__(self, attr: Any) -> Any: + def __getattr__(self, attr: Any) -> _TObject: """Get an attribute by id.""" try: return DictList.get_by_id(self, attr) From 854e71c3db776549d893a6cbb1293379806b84e8 Mon Sep 17 00:00:00 2001 From: abates20 <131566916+abates20@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:34:00 -0500 Subject: [PATCH 5/5] refactor: Change name of DictList TypeVar from _TObject to CobraObject. --- src/cobra/core/dictlist.py | 66 ++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/cobra/core/dictlist.py b/src/cobra/core/dictlist.py index 77c602207..0684b1a34 100644 --- a/src/cobra/core/dictlist.py +++ b/src/cobra/core/dictlist.py @@ -19,10 +19,10 @@ from .object import Object -_TObject = TypeVar("_TObject", bound=Object) +CobraObject = TypeVar("CobraObject", bound=Object) -class DictList(List[_TObject]): +class DictList(List[CobraObject]): """ Define a combined dict and list. @@ -53,12 +53,12 @@ def __init__(self, *args): self.extend(other) # noinspection PyShadowingBuiltins - def has_id(self, id: Union[_TObject, str]) -> bool: + def has_id(self, id: Union[CobraObject, str]) -> bool: """Check if id is in DictList.""" return id in self._dict # noinspection PyShadowingBuiltins - def _check(self, id: Union[_TObject, str]) -> None: + def _check(self, id: Union[CobraObject, str]) -> None: """Make sure duplicate id's are not added. This function is called before adding in elements. @@ -72,7 +72,7 @@ def _generate_index(self) -> None: self._dict = {v.id: k for k, v in enumerate(self)} # noinspection PyShadowingBuiltins - def get_by_id(self, id: Union[_TObject, str]) -> _TObject: + def get_by_id(self, id: Union[CobraObject, str]) -> CobraObject: """Return the element with a matching id.""" return list.__getitem__(self, self._dict[id]) @@ -80,7 +80,9 @@ def list_attr(self, attribute: str) -> list: """Return a list of the given attribute for every object.""" return [getattr(i, attribute) for i in self] - def get_by_any(self, iterable: List[Union[str, _TObject, int]]) -> List[_TObject]: + def get_by_any( + self, iterable: List[Union[str, CobraObject, int]] + ) -> List[CobraObject]: """Get a list of members using several different ways of indexing. Parameters @@ -96,7 +98,7 @@ def get_by_any(self, iterable: List[Union[str, _TObject, int]]) -> List[_TObject a list of members """ - def get_item(item: Any) -> _TObject: + def get_item(item: Any) -> CobraObject: if isinstance(item, int): return self[item] elif isinstance(item, str): @@ -114,7 +116,7 @@ def query( self, search_function: Union[str, Pattern, Callable], attribute: Union[str, None] = None, - ) -> "DictList[_TObject]": + ) -> "DictList[CobraObject]": """Query the list. Parameters @@ -171,21 +173,21 @@ def select_attribute(x: Optional[Any]) -> Any: results._extend_nocheck(matches) return results - def _replace_on_id(self, new_object: _TObject) -> None: + def _replace_on_id(self, new_object: CobraObject) -> None: """Replace an object by another with the same id.""" the_id = new_object.id the_index = self._dict[the_id] list.__setitem__(self, the_index, new_object) # overriding default list functions with new ones - def append(self, entity: _TObject) -> None: + def append(self, entity: CobraObject) -> None: """Append object to end.""" the_id = entity.id self._check(the_id) self._dict[the_id] = len(self) list.append(self, entity) - def union(self, iterable: Iterable[_TObject]) -> None: + def union(self, iterable: Iterable[CobraObject]) -> None: """Add elements with id's not already in the model.""" _dict = self._dict append = self.append @@ -193,7 +195,7 @@ def union(self, iterable: Iterable[_TObject]) -> None: if i.id not in _dict: append(i) - def extend(self, iterable: Iterable[_TObject]) -> None: + def extend(self, iterable: Iterable[CobraObject]) -> None: """Extend list by appending elements from the iterable. Sometimes during initialization from an older pickle, _dict @@ -226,7 +228,7 @@ def extend(self, iterable: Iterable[_TObject]) -> None: f"Is it present twice?" ) - def _extend_nocheck(self, iterable: Iterable[_TObject]) -> None: + def _extend_nocheck(self, iterable: Iterable[CobraObject]) -> None: """Extend without checking for uniqueness. This function should only be used internally by DictList when it @@ -248,7 +250,7 @@ def _extend_nocheck(self, iterable: Iterable[_TObject]) -> None: for i, obj in enumerate(islice(self, current_length, None), current_length): _dict[obj.id] = i - def __sub__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": + def __sub__(self, other: Iterable[CobraObject]) -> "DictList[CobraObject]": """Remove a value or values, and returns the new DictList. x.__sub__(y) <==> x - y @@ -268,7 +270,7 @@ def __sub__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": total.remove(item) return total - def __isub__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": + def __isub__(self, other: Iterable[CobraObject]) -> "DictList[CobraObject]": """Remove a value or values in place. x.__sub__(y) <==> x -= y @@ -282,7 +284,7 @@ def __isub__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": self.remove(item) return self - def __add__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": + def __add__(self, other: Iterable[CobraObject]) -> "DictList[CobraObject]": """Add item while returning a new DictList. x.__add__(y) <==> x + y @@ -298,7 +300,7 @@ def __add__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": total.extend(other) return total - def __iadd__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": + def __iadd__(self, other: Iterable[CobraObject]) -> "DictList[CobraObject]": """Add item while returning the same DictList. x.__iadd__(y) <==> x += y @@ -313,7 +315,7 @@ def __iadd__(self, other: Iterable[_TObject]) -> "DictList[_TObject]": self.extend(other) return self - def __reduce__(self) -> Tuple[Type["DictList"], Tuple, dict, Iterator[_TObject]]: + def __reduce__(self) -> Tuple[Type["DictList"], Tuple, dict, Iterator[CobraObject]]: """Return a reduced version of DictList. This reduced version details the class, an empty Tuple, a dictionary of the @@ -340,7 +342,7 @@ def __setstate__(self, state: dict) -> None: self._generate_index() # noinspection PyShadowingBuiltins - def index(self, id: Union[str, _TObject], *args) -> int: + def index(self, id: Union[str, CobraObject], *args) -> int: """Determine the position in the list. Parameters @@ -364,7 +366,7 @@ def index(self, id: Union[str, _TObject], *args) -> int: except KeyError: raise ValueError(f"{str(id)} not found") - def __contains__(self, entity: Union[str, _TObject]) -> bool: + def __contains__(self, entity: Union[str, CobraObject]) -> bool: """Ask if the DictList contain an entity. DictList.__contains__(entity) <==> entity in DictList @@ -381,14 +383,14 @@ def __contains__(self, entity: Union[str, _TObject]) -> bool: the_id = entity return the_id in self._dict - def __copy__(self) -> "DictList[_TObject]": + def __copy__(self) -> "DictList[CobraObject]": """Copy the DictList into a new one.""" the_copy = DictList() list.extend(the_copy, self) the_copy._dict = self._dict.copy() return the_copy - def insert(self, index: int, entity: _TObject) -> None: + def insert(self, index: int, entity: CobraObject) -> None: """Insert entity before index.""" self._check(entity.id) list.insert(self, index, entity) @@ -399,7 +401,7 @@ def insert(self, index: int, entity: _TObject) -> None: _dict[i] = j + 1 _dict[entity.id] = index - def pop(self, *args) -> _TObject: + def pop(self, *args) -> CobraObject: """Remove and return item at index (default last).""" value = list.pop(self, *args) index = self._dict.pop(value.id) @@ -413,11 +415,11 @@ def pop(self, *args) -> _TObject: _dict[i] = j - 1 return value - def add(self, x: _TObject) -> None: + def add(self, x: CobraObject) -> None: """Opposite of `remove`. Mirrors set.add.""" self.extend([x]) - def remove(self, x: Union[str, _TObject]) -> None: + def remove(self, x: Union[str, CobraObject]) -> None: """.. warning :: Internal use only. Each item is unique in the list which allows this @@ -449,8 +451,8 @@ def key(i): self._generate_index() def __getitem__( - self, i: Union[int, slice, Iterable, _TObject, "DictList[_TObject]"] - ) -> Union["DictList[_TObject]", _TObject]: + self, i: Union[int, slice, Iterable, CobraObject, "DictList[CobraObject]"] + ) -> Union["DictList[CobraObject]", CobraObject]: """Get item from DictList.""" if isinstance(i, int): return list.__getitem__(self, i) @@ -470,7 +472,7 @@ def __getitem__( return list.__getitem__(self, i) def __setitem__( - self, i: Union[slice, int], y: Union[List[_TObject], _TObject] + self, i: Union[slice, int], y: Union[List[CobraObject], CobraObject] ) -> None: """Set an item via index or slice. @@ -513,11 +515,13 @@ def __delitem__(self, index: int) -> None: if j > index: _dict[i] = j - 1 - def __getslice__(self, i: int, j: int) -> "DictList[_TObject]": + def __getslice__(self, i: int, j: int) -> "DictList[CobraObject]": """Get a slice from it to j of DictList.""" return self.__getitem__(slice(i, j)) - def __setslice__(self, i: int, j: int, y: Union[List[_TObject], _TObject]) -> None: + def __setslice__( + self, i: int, j: int, y: Union[List[CobraObject], CobraObject] + ) -> None: """Set slice, where y is an iterable.""" self.__setitem__(slice(i, j), y) @@ -525,7 +529,7 @@ def __delslice__(self, i: int, j: int) -> None: """Remove slice.""" self.__delitem__(slice(i, j)) - def __getattr__(self, attr: Any) -> _TObject: + def __getattr__(self, attr: Any) -> CobraObject: """Get an attribute by id.""" try: return DictList.get_by_id(self, attr)