From fe7e44a3a6cfc8f86ab20003fc34fe14ed2975fa Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 20 Jul 2023 16:00:16 +0200
Subject: [PATCH] Add annotations to tree and item classes
---
novelwriter/core/item.py | 187 ++++++++++++++----------------
novelwriter/core/tree.py | 157 ++++++++++---------------
tests/test_core/test_core_item.py | 8 --
tests/test_core/test_core_tree.py | 10 +-
4 files changed, 150 insertions(+), 212 deletions(-)
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index dfee50a3..7ec29852 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -22,15 +22,23 @@ General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from __future__ import annotations
import logging
+from typing import TYPE_CHECKING, Any
+
+from PyQt5.QtGui import QIcon
+
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
)
from novelwriter.constants import nwHeaders, nwLabels, trConst
+if TYPE_CHECKING: # pragma: no cover
+ from novelwriter.core.project import NWProject
+
logger = logging.getLogger(__name__)
@@ -43,7 +51,7 @@ class NWItem:
"_paraCount", "_cursorPos", "_initCount",
)
- def __init__(self, project):
+ def __init__(self, project: NWProject) -> None:
self._project = project
self._name = ""
@@ -69,10 +77,10 @@ class NWItem:
return
- def __repr__(self):
+ def __repr__(self) -> str:
return f""
- def __bool__(self):
+ def __bool__(self) -> bool:
return self._handle is not None
##
@@ -80,87 +88,86 @@ class NWItem:
##
@property
- def itemName(self):
+ def itemName(self) -> str:
return self._name
@property
- def itemHandle(self):
+ def itemHandle(self) -> str | None:
return self._handle
@property
- def itemParent(self):
+ def itemParent(self) -> str | None:
return self._parent
@property
- def itemRoot(self):
+ def itemRoot(self) -> str | None:
return self._root
@property
- def itemOrder(self):
+ def itemOrder(self) -> int:
return self._order
@property
- def itemType(self):
+ def itemType(self) -> nwItemType:
return self._type
@property
- def itemClass(self):
+ def itemClass(self) -> nwItemClass:
return self._class
@property
- def itemLayout(self):
+ def itemLayout(self) -> nwItemLayout:
return self._layout
@property
- def itemStatus(self):
+ def itemStatus(self) -> str | None:
return self._status
@property
- def itemImport(self):
+ def itemImport(self) -> str | None:
return self._import
@property
- def isActive(self):
+ def isActive(self) -> bool:
return self._active
@property
- def isExpanded(self):
+ def isExpanded(self) -> bool:
return self._expanded
@property
- def mainHeading(self):
+ def mainHeading(self) -> str:
return self._heading
@property
- def charCount(self):
+ def charCount(self) -> int:
return self._charCount
@property
- def wordCount(self):
+ def wordCount(self) -> int:
return self._wordCount
@property
- def paraCount(self):
+ def paraCount(self) -> int:
return self._paraCount
@property
- def initCount(self):
+ def initCount(self) -> int:
return self._initCount
@property
- def cursorPos(self):
+ def cursorPos(self) -> int:
return self._cursorPos
##
# Pack/Unpack Data
##
- def pack(self):
- """Pack all the data in the class instance into a dictionary.
- """
- item = {}
- meta = {}
- name = {}
+ def pack(self) -> dict[str, dict[str, str]]:
+ """Pack all the data in the class instance into a dictionary."""
+ item: dict[str, str] = {}
+ meta: dict[str, str] = {}
+ name: dict[str, str] = {}
item["handle"] = str(self._handle)
item["parent"] = str(self._parent)
@@ -190,9 +197,8 @@ class NWItem:
return data
- def unpack(self, data):
- """Set the values from a data dictionary.
- """
+ def unpack(self, data: dict[str, dict[str, Any]]) -> bool:
+ """Set the values from a data dictionary."""
item = data.get("itemAttr", {})
meta = data.get("metaAttr", {})
name = data.get("nameAttr", {})
@@ -243,9 +249,8 @@ class NWItem:
# Lookup Methods
##
- def describeMe(self):
- """Return a string description of the item.
- """
+ def describeMe(self) -> str:
+ """Return a string description of the item."""
descKey = "none"
if self._type == nwItemType.ROOT:
descKey = "root"
@@ -268,7 +273,7 @@ class NWItem:
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
- def getImportStatus(self, incIcon=True):
+ def getImportStatus(self, incIcon: bool = True) -> tuple[str, QIcon | None]:
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
@@ -284,51 +289,43 @@ class NWItem:
# Checker Methods
##
- def isNovelLike(self):
- """Returns true if the item is of a novel-like class.
- """
+ def isNovelLike(self) -> bool:
+ """Check if the item is of a novel-like class."""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE)
- def documentAllowed(self):
- """Returns true if the item is allowed to be of document layout.
- """
+ def documentAllowed(self) -> bool:
+ """Check if the item is allowed to be of document layout."""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH)
- def isInactiveClass(self):
- """Returns true if the item is in an inactive class.
- """
+ def isInactiveClass(self) -> bool:
+ """Check if the item is in an inactive class."""
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
- def isRootType(self):
+ def isRootType(self) -> bool:
+ """Check if item is a root item."""
return self._type == nwItemType.ROOT
- def isFolderType(self):
+ def isFolderType(self) -> bool:
+ """Check if item is a folder item."""
return self._type == nwItemType.FOLDER
- def isFileType(self):
+ def isFileType(self) -> bool:
+ """Check if item is a file item."""
return self._type == nwItemType.FILE
- def isNoteLayout(self):
+ def isNoteLayout(self) -> bool:
+ """Check if item is a project note."""
return self._layout == nwItemLayout.NOTE
- def isDocumentLayout(self):
+ def isDocumentLayout(self) -> bool:
+ """Check if item is a novel document."""
return self._layout == nwItemLayout.DOCUMENT
##
# Special Setters
##
- def setImportStatus(self, value):
- """Update the importance or status value based on class. This is
- a wrapper setter for setStatus and setImport.
- """
- if self.isNovelLike():
- self.setStatus(value)
- else:
- self.setImport(value)
- return
-
- def setClassDefaults(self, itemClass):
+ def setClassDefaults(self, itemClass: nwItemClass) -> None:
"""Set the default values based on the item's class and the
project settings.
"""
@@ -358,27 +355,24 @@ class NWItem:
# Set Item Values
##
- def setName(self, name):
- """Set the item name.
- """
+ def setName(self, name: Any) -> None:
+ """Set the item name."""
if isinstance(name, str):
self._name = simplified(name)
else:
self._name = ""
return
- def setHandle(self, handle):
- """Set the item handle, and ensure it is valid.
- """
+ def setHandle(self, handle: Any) -> None:
+ """Set the item handle, and ensure it is valid."""
if isHandle(handle):
self._handle = handle
else:
self._handle = None
return
- def setParent(self, handle):
- """Set the parent handle, and ensure it is valid.
- """
+ def setParent(self, handle: Any) -> None:
+ """Set the parent handle, and ensure it is valid."""
if handle is None:
self._parent = None
elif isHandle(handle):
@@ -387,9 +381,8 @@ class NWItem:
self._parent = None
return
- def setRoot(self, handle):
- """Set the root handle, and ensure it is valid.
- """
+ def setRoot(self, handle: Any) -> None:
+ """Set the root handle, and ensure it is valid."""
if handle is None:
self._root = None
elif isHandle(handle):
@@ -398,7 +391,7 @@ class NWItem:
self._root = None
return
- def setOrder(self, order):
+ def setOrder(self, order: Any) -> None:
"""Set the item order, and ensure that it is valid. This value
is purely a meta value, and not actually used by novelWriter at
the moment.
@@ -406,7 +399,7 @@ class NWItem:
self._order = checkInt(order, 0)
return
- def setType(self, value):
+ def setType(self, value: Any) -> None:
"""Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType.
"""
@@ -419,7 +412,7 @@ class NWItem:
self._type = nwItemType.NO_TYPE
return
- def setClass(self, value):
+ def setClass(self, value: Any) -> None:
"""Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass.
"""
@@ -432,7 +425,7 @@ class NWItem:
self._class = nwItemClass.NO_CLASS
return
- def setLayout(self, value):
+ def setLayout(self, value: Any) -> None:
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout.
"""
@@ -445,32 +438,30 @@ class NWItem:
self._layout = nwItemLayout.NO_LAYOUT
return
- def setStatus(self, value):
+ def setStatus(self, value: Any) -> None:
"""Set the item status by looking it up in the valid status
items of the current project.
"""
self._status = self._project.data.itemStatus.check(value)
return
- def setImport(self, value):
+ def setImport(self, value: Any) -> None:
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self._project.data.itemImport.check(value)
return
- def setActive(self, state):
- """Set the active flag.
- """
+ def setActive(self, state: Any) -> None:
+ """Set the active flag."""
if isinstance(state, bool):
self._active = state
else:
self._active = False
return
- def setExpanded(self, state):
- """Set the expanded status of an item in the project tree.
- """
+ def setExpanded(self, state: Any) -> None:
+ """Set the expanded status of an item in the project tree."""
if isinstance(state, bool):
self._expanded = state
else:
@@ -481,52 +472,46 @@ class NWItem:
# Set Document Meta Data
##
- def setMainHeading(self, value):
- """Set the main heading level.
- """
+ def setMainHeading(self, value: str) -> None:
+ """Set the main heading level."""
if value in nwHeaders.H_LEVEL:
self._heading = value
return
- def setCharCount(self, count):
- """Set the character count, and ensure that it is an integer.
- """
+ def setCharCount(self, count: Any) -> None:
+ """Set the character count, and ensure that it is an integer."""
if isinstance(count, int):
self._charCount = max(0, count)
else:
self._charCount = 0
return
- def setWordCount(self, count):
- """Set the word count, and ensure that it is an integer.
- """
+ def setWordCount(self, count: Any) -> None:
+ """Set the word count, and ensure that it is an integer."""
if isinstance(count, int):
self._wordCount = max(0, count)
else:
self._wordCount = 0
return
- def setParaCount(self, count):
- """Set the paragraph count, and ensure that it is an integer.
- """
+ def setParaCount(self, count: Any) -> None:
+ """Set the paragraph count, and ensure that it is an integer."""
if isinstance(count, int):
self._paraCount = max(0, count)
else:
self._paraCount = 0
return
- def setCursorPos(self, position):
- """Set the cursor position, and ensure that it is an integer.
- """
+ def setCursorPos(self, position: Any) -> None:
+ """Set the cursor position, and ensure that it is an integer."""
if isinstance(position, int):
self._cursorPos = max(0, position)
else:
self._cursorPos = 0
return
- def saveInitialCount(self):
- """Save the initial word count.
- """
+ def saveInitialCount(self) -> None:
+ """Save the initial word count."""
self._initCount = self._wordCount
return
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index da5686db..14501940 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -22,18 +22,23 @@ General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from __future__ import annotations
import random
import logging
+from typing import TYPE_CHECKING, Any, Iterator
from pathlib import Path
-from novelwriter.enum import nwItemClass, nwItemLayout
+from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
from novelwriter.common import checkHandle
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
+if TYPE_CHECKING: # pragma: no cover
+ from novelwriter.core.project import NWProject
+
logger = logging.getLogger(__name__)
@@ -41,15 +46,16 @@ class NWTree:
MAX_DEPTH = 1000 # Cap of tree traversing for loops
- def __init__(self, theProject):
+ def __init__(self, project: NWProject) -> None:
- self.theProject = theProject
+ self._project = project
- self._projTree = {} # Holds all the items of the project
- self._treeOrder = [] # The order of the tree items on the tree view
- self._treeRoots = {} # The root items of the tree
- self._trashRoot = None # The handle of the trash root folder
- self._archRoot = None # The handle of the archive root folder
+ self._projTree: dict[str, NWItem] = {} # Holds all the items of the project
+ self._treeOrder: list[str] = [] # The order of the tree items on the tree view
+ self._treeRoots: dict[str, NWItem] = {} # The root items of the tree
+
+ self._trashRoot = None # The handle of the trash root folder
+ self._archRoot = None # The handle of the archive root folder
self._treeChanged = False # True if tree structure has changed
return
@@ -58,9 +64,8 @@ class NWTree:
# Class Methods
##
- def clear(self):
- """Clear the item tree entirely.
- """
+ def clear(self) -> None:
+ """Clear the item tree entirely."""
self._projTree = {}
self._treeOrder = []
self._treeRoots = {}
@@ -69,14 +74,12 @@ class NWTree:
self._treeChanged = False
return
- def handles(self):
- """Returns a copy of the list of all the active handles.
- """
+ def handles(self) -> list[str]:
+ """Returns a copy of the list of all the active handles."""
return self._treeOrder.copy()
- def append(self, tHandle, pHandle, nwItem):
- """Add a new item to the end of the tree.
- """
+ def append(self, tHandle: str | None, pHandle: str | None, nwItem: NWItem) -> bool:
+ """Add a new item to the end of the tree."""
tHandle = checkHandle(tHandle, None, True)
pHandle = checkHandle(pHandle, None, True)
if tHandle is None:
@@ -111,9 +114,9 @@ class NWTree:
return True
- def pack(self):
- """Pack the content of the tree into the provided XML object. In
- the order defined by the _treeOrder list.
+ def pack(self) -> list[dict[str, dict[str, str]]]:
+ """Pack the content of the tree into a list of doctionaries of
+ items. In the order defined by the _treeOrder list.
"""
tree = []
for tHandle in self._treeOrder:
@@ -122,25 +125,24 @@ class NWTree:
tree.append(tItem.pack())
return tree
- def unpack(self, data):
+ def unpack(self, data: list[dict[str, dict[str, Any]]]) -> None:
"""Iterate through all items of a list and add them to the
project tree.
"""
self.clear()
for item in data:
- nwItem = NWItem(self.theProject)
+ nwItem = NWItem(self._project)
if nwItem.unpack(item):
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
nwItem.saveInitialCount()
+ return
- return True
-
- def writeToCFile(self):
+ def writeToCFile(self) -> bool:
"""Write the convenience table of contents file in the root of
the project directory.
"""
- runtimePath = self.theProject.storage.runtimePath
- contentPath = self.theProject.storage.contentPath
+ runtimePath = self._project.storage.runtimePath
+ contentPath = self._project.storage.contentPath
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
return False
@@ -184,9 +186,8 @@ class NWTree:
return True
- def sumWords(self):
- """Loop over all entries and add up the word counts.
- """
+ def sumWords(self) -> tuple[int, int]:
+ """Loop over all entries and add up the word counts."""
noteWords = 0
novelWords = 0
for tHandle in self._treeOrder:
@@ -205,7 +206,7 @@ class NWTree:
# Tree Item Methods
##
- def updateItemData(self, tHandle):
+ def updateItemData(self, tHandle: str) -> bool:
"""Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False.
"""
@@ -226,15 +227,14 @@ class NWTree:
else:
raise RecursionError("Critical internal error")
- def checkType(self, tHandle, itemType):
- """Return true of item exists and is of the specified item type.
- """
+ def checkType(self, tHandle: str, itemType: nwItemType) -> bool:
+ """Check if item exists and is of the specified item type."""
tItem = self.__getitem__(tHandle)
if not tItem:
return False
return tItem.itemType == itemType
- def getItemPath(self, tHandle):
+ def getItemPath(self, tHandle: str) -> list[str]:
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles.
We do this with a for loop with a maximum depth to make
@@ -263,17 +263,15 @@ class NWTree:
# Tree Root Methods
##
- def rootClasses(self):
- """Return a set of all root classes in use by the project.
- """
+ def rootClasses(self) -> set[nwItemClass]:
+ """Return a set of all root classes in use by the project."""
rootClasses = set()
for nwItem in self._treeRoots.values():
rootClasses.add(nwItem.itemClass)
return rootClasses
- def iterRoots(self, itemClass):
- """Iterate over all root items of a given class in order.
- """
+ def iterRoots(self, itemClass: nwItemClass | None) -> Iterator[tuple[str, NWItem]]:
+ """Iterate over all root items of a given class in order."""
for tHandle in self._treeOrder:
nwItem = self.__getitem__(tHandle)
if isinstance(nwItem, NWItem) and nwItem.isRootType():
@@ -281,14 +279,8 @@ class NWTree:
yield tHandle, nwItem
return
- def isRoot(self, tHandle):
- """Check if a handle is a root item.
- """
- return tHandle in self._treeRoots
-
- def isTrash(self, tHandle):
- """Check if an item is in or is the trash folder.
- """
+ def isTrash(self, tHandle: str) -> bool:
+ """Check if an item is in or is the trash folder."""
tItem = self.__getitem__(tHandle)
if tItem is None:
return True
@@ -303,7 +295,7 @@ class NWTree:
return True
return False
- def trashRoot(self):
+ def trashRoot(self) -> str | None:
"""Returns the handle of the trash folder, or None if there
isn't one.
"""
@@ -311,14 +303,13 @@ class NWTree:
return self._trashRoot
return None
- def findRoot(self, theClass):
- """Find the first root item for a given class.
- """
+ def findRoot(self, itemClass: nwItemClass) -> str | None:
+ """Find the first root item for a given class."""
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
- if theClass == tItem.itemClass:
+ if itemClass == tItem.itemClass:
return tItem.itemHandle
return None
@@ -326,9 +317,8 @@ class NWTree:
# Setters
##
- def setOrder(self, newOrder):
- """Reorders the tree based on a list of items.
- """
+ def setOrder(self, newOrder: list[str]) -> None:
+ """Reorders the tree based on a list of items."""
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree]
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)):
# Something is wrong, so let's debug it
@@ -346,37 +336,19 @@ class NWTree:
return
- def setFileItemLayout(self, tHandle, itemLayout):
- """Set the nwItemLayout for a specific file.
- """
- tItem = self.__getitem__(tHandle)
- if tItem is None:
- return False
- if not tItem.isFileType():
- logger.error("Item '%s' is not a file", tHandle)
- return False
- if not isinstance(itemLayout, nwItemLayout):
- return False
-
- tItem.setLayout(itemLayout)
-
- return True
-
##
# Special Methods
##
- def __len__(self):
- """The number of items in the project.
- """
+ def __len__(self) -> int:
+ """The number of items in the project."""
return len(self._treeOrder)
- def __bool__(self):
- """True if there are any items in the project.
- """
+ def __bool__(self) -> bool:
+ """True if there are any items in the project."""
return bool(self._treeOrder)
- def __getitem__(self, tHandle):
+ def __getitem__(self, tHandle: str) -> NWItem | None:
"""Return a project item based on its handle. Returns None if
the handle doesn't exist in the project.
"""
@@ -385,9 +357,8 @@ class NWTree:
logger.error("No tree item with handle '%s'", str(tHandle))
return None
- def __delitem__(self, tHandle):
- """Remove an item from the internal lists and dictionaries.
- """
+ def __delitem__(self, tHandle: str) -> None:
+ """Remove an item from the internal lists and dictionaries."""
if tHandle in self._treeOrder and tHandle in self._projTree:
self._treeOrder.remove(tHandle)
del self._projTree[tHandle]
@@ -406,14 +377,12 @@ class NWTree:
return
- def __contains__(self, tHandle):
- """Checks if a handle exists in the tree.
- """
+ def __contains__(self, tHandle: str) -> bool:
+ """Checks if a handle exists in the tree."""
return tHandle in self._treeOrder
- def __iter__(self):
- """Iterate through project items.
- """
+ def __iter__(self) -> Iterator[NWItem]:
+ """Iterate through project items."""
for tHandle in self._treeOrder:
tItem = self._projTree.get(tHandle)
if isinstance(tItem, NWItem):
@@ -424,16 +393,16 @@ class NWTree:
# Internal Functions
##
- def _setTreeChanged(self, theState):
+ def _setTreeChanged(self, state: bool) -> None:
"""Set the changed flag to theState, and if being set to True,
propagate that state change to the parent NWProject class.
"""
- self._treeChanged = theState
- if theState:
- self.theProject.setProjectChanged(True)
+ self._treeChanged = state
+ if state:
+ self._project.setProjectChanged(True)
return
- def _makeHandle(self):
+ def _makeHandle(self) -> str:
"""Generate a unique item handle. In the event that the key
already exists, generate a new one.
"""
diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py
index b388a548..9fc90dad 100644
--- a/tests/test_core/test_core_item.py
+++ b/tests/test_core/test_core_item.py
@@ -262,19 +262,11 @@ def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
assert stT == "Note"
assert isinstance(stI, QIcon)
- theItem.setImportStatus(C.sDraft)
- stT, stI = theItem.getImportStatus()
- assert stT == "Draft"
-
theItem.setClass("CHARACTER")
stT, stI = theItem.getImportStatus()
assert stT == "Minor"
assert isinstance(stI, QIcon)
- theItem.setImportStatus(C.iMajor)
- stT, stI = theItem.getImportStatus()
- assert stT == "Major"
-
# Representation
# ==============
diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py
index ee9ce687..746e0539 100644
--- a/tests/test_core/test_core_tree.py
+++ b/tests/test_core/test_core_tree.py
@@ -149,7 +149,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree.trashRoot() == "a000000000003"
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
assert theTree.isTrash("a000000000003") is True
- assert theTree.isRoot("a000000000002") is True
# Check that we have the root classes
assert theTree.rootClasses() == {
@@ -255,7 +254,7 @@ def testCoreTree_PackUnpack(mockGUI, mockItems):
theTree.clear()
assert len(theTree) == 0
assert theTree.handles() == []
- assert theTree.unpack(tree) is True
+ theTree.unpack(tree)
assert theTree.handles() == aHandles
# END Test testCoreTree_PackUnpack
@@ -339,13 +338,6 @@ def testCoreTree_Methods(mockGUI, mockItems):
"c000000000001", "b000000000001", "a000000000001"
]
- # Change file layout
- assert theTree.setFileItemLayout("stuff", nwItemLayout.DOCUMENT) is False
- assert theTree.setFileItemLayout("b000000000001", nwItemLayout.DOCUMENT) is False
- assert theTree.setFileItemLayout("c000000000001", "stuff") is False
- assert theTree.setFileItemLayout("c000000000001", nwItemLayout.NOTE) is True
- assert theTree["c000000000001"].itemLayout == nwItemLayout.NOTE
-
# END Test testCoreTree_Methods