From f547da4cf16091cd7fe6850534b4aeb57fb771eb Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@jrother.eu>
Date: Tue, 23 Nov 2021 16:23:13 +0100
Subject: [PATCH] Fixed, updated and extended api documentation

---
 docs/api.rst                     | 180 +++++++++++++++++++++++++++----
 ldapserver/dn.py                 | 149 ++++++++++++++++++++++---
 ldapserver/exceptions.py         |  70 ++++++++++++
 ldapserver/ldap.py               |  84 +++++++--------
 ldapserver/objects.py            |  89 ++++++++++++++-
 ldapserver/schema/__init__.py    |  24 ++++-
 ldapserver/schema/definitions.py | 127 ++++++++++++++++++++--
 ldapserver/schema/types.py       | 169 +++++++++++++++++++++++++++--
 ldapserver/server.py             | 134 +++++++++++------------
 9 files changed, 855 insertions(+), 171 deletions(-)

diff --git a/docs/api.rst b/docs/api.rst
index 9b39ba7..6aeda77 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -4,26 +4,150 @@ API Reference
 Request Handler
 ---------------
 
-.. autoclass:: ldapserver.BaseLDAPRequestHandler
-  :members:
+.. autoclass:: ldapserver.LDAPRequestHandler
+	:members:
+	:exclude-members: do_bind_simple, do_bind_sasl
 
-.. autoclass:: ldapserver.SimpleLDAPRequestHandler
-  :members:
+	.. autoattribute:: logger
 
 Distinguished Names
 -------------------
 
-.. automodule:: ldapserver.dn
-  :members:
+.. autoclass:: ldapserver.DN
+	:members:
+	:special-members: __str__
+
+.. autoclass:: ldapserver.RDN
+	:members:
+	:special-members: __str__
+
+.. autoclass:: ldapserver.RDNAssertion
+	:members:
+	:special-members: __str__
+
+.. autoclass:: ldapserver.DNWithUID
+	:members:
+	:special-members: __str__
+
+Objects
+-------
+
+.. autoclass:: ldapserver.AttributeDict
+	:members:
+
+.. autoclass:: ldapserver.Object
+	:members:
+
+.. autoclass:: ldapserver.RootDSE
+	:members:
+
+.. autoclass:: ldapserver.SubschemaSubentry
+	:members:
+
+.. autodata:: ldapserver.WILDCARD_VALUE
+	:no-value:
+
+.. autoclass:: ldapserver.ObjectTemplate
+	:members:
+
+Schema
+------
+
+The :class:`schema.Schema` class and the schema-bound classes like :class:`schema.Syntax` provide a high-level interface to the low-level schema definition objects.
+
+Objects of the schema-bound classes reference other objects within their schema directly.
+They are automatically instanciated when a :any:`schema.Schema` object is created based on the passed definition objects.
+
+.. autoclass:: ldapserver.schema.Schema
+	:members:
+
+.. autoclass:: ldapserver.schema.ObjectClass
+	:members:
+
+.. autoclass:: ldapserver.schema.AttributeType
+	:members:
+
+.. autoclass:: ldapserver.schema.EqualityMatchingRule
+	:members:
+	:inherited-members:
+
+.. autoclass:: ldapserver.schema.OrderingMatchingRule
+	:members:
+	:inherited-members:
+
+.. autoclass:: ldapserver.schema.SubstrMatchingRule
+	:members:
+	:inherited-members:
+
+.. autoclass:: ldapserver.schema.Syntax
+	:members:
+
+Low-level Schema Definitions
+----------------------------
+
+:class:`schema.AttributeTypeDefinition` and :class:`schema.ObjectClassDefinition` objects are nothing more than parsed representations of attribute type and object class definition strings found in standard documents such as RFC4519.
+:class:`schema.MatchingRuleDefinition` and :class:`schema.SyntaxDefinition` also provide methods for encoding/decoding or matching.
+
+Instance attributes of definition objects are named after the definition string keywords converted to snake-case (e.g. `NAME` becomes `name`, `NO-USER-MODIFICATION` becomes `no_user_modification`).
+
+.. autoclass:: ldapserver.schema.SyntaxDefinition
+	:members:
+	:special-members: __str__
+
+.. autoclass:: ldapserver.schema.MatchingRuleDefinition
+	:members:
+	:special-members: __str__
+
+.. autoclass:: ldapserver.schema.MatchingRuleUseDefinition
+	:members:
+	:special-members: __str__
+
+.. autoclass:: ldapserver.schema.AttributeTypeUsage
+	:members:
+
+.. autoclass:: ldapserver.schema.AttributeTypeDefinition
+	:members:
+	:special-members: __str__
+
+.. autoclass:: ldapserver.schema.ObjectClassKind
+	:members:
+
+.. autoclass:: ldapserver.schema.ObjectClassDefinition
+	:members:
+	:special-members: __str__
+
+Built-in Schemas
+----------------
+
+.. autodata:: ldapserver.schema.RFC4512_SCHEMA
+	:no-value:
+.. autodata:: ldapserver.schema.CORE_SCHEMA
+	:annotation: = RFC4512_SCHEMA
 
-Directory Objects
------------------
+.. autodata:: ldapserver.schema.RFC4519_SCHEMA
+	:no-value:
 
-.. autoclass:: ldapserver.directory.BaseDirectory
-  :members:
+.. autodata:: ldapserver.schema.RFC4523_SCHEMA
+  :no-value:
 
-.. autoclass:: ldapserver.directory.FilterMixin
-  :members:
+.. autodata:: ldapserver.schema.RFC4524_SCHEMA
+	:no-value:
+.. autodata:: ldapserver.schema.COSINE_SCHEMA
+	:annotation: = RFC4524_SCHEMA
+
+.. autodata:: ldapserver.schema.RFC3112_SCHEMA
+	:no-value:
+
+.. autodata:: ldapserver.schema.RFC2079_SCHEMA
+	:no-value:
+
+.. autodata:: ldapserver.schema.RFC2798_SCHEMA
+	:no-value:
+.. autodata:: ldapserver.schema.INETORG_SCHMEA
+	:annotation: = RFC2798_SCHEMA
+
+.. autodata:: ldapserver.schema.RFC2307BIS_SCHEMA
+	:no-value:
 
 LDAP Protocol
 -------------
@@ -33,24 +157,44 @@ LDAP Protocol
 	:undoc-members:
 
 .. autoclass:: ldapserver.ldap.Filter
+	:members:
 .. autoclass:: ldapserver.ldap.FilterAnd
+	:members:
 .. autoclass:: ldapserver.ldap.FilterOr
+	:members:
 .. autoclass:: ldapserver.ldap.FilterNot
-.. autoclass:: ldapserver.ldap.FilterEqual
+	:members:
 .. autoclass:: ldapserver.ldap.FilterPresent
+	:members:
+.. autoclass:: ldapserver.ldap.FilterEqual
+	:members:
+.. autoclass:: ldapserver.ldap.FilterApproxMatch
+	:members:
+.. autoclass:: ldapserver.ldap.FilterGreaterOrEqual
+	:members:
+.. autoclass:: ldapserver.ldap.FilterLessOrEqual
+	:members:
+.. autoclass:: ldapserver.ldap.FilterSubstrings
+	:members:
+.. autoclass:: ldapserver.ldap.FilterExtensibleMatch
+	:members:
 
-.. autoclass:: ldapserver.ldap.LDAPMessage
+.. autoclass:: ldapserver.ldap.SearchResultEntry
+	:members:
+.. autoclass:: ldapserver.ldap.PartialAttribute
 	:members:
 
 LDAP Errors
 -----------
 
 LDAP response messages carry a result code and an optional diagnostic message.
-The subclasses of :any:`ldapserver.exceptions.LDAPError` represent the possible (non-success) result codes.
+The subclasses of :any:`ldapserver.exceptions.LDAPError` represent the
+possible (non-success) result codes.
 
-Raising a :any:`ldapserver.exceptions.LDAPError` instance in a handler method of
-:any:`ldapserver.BaseLDAPRequestHandler` cases the appropriate response message to be
-sent with the corresponding result code and diagnostic message.
+Raising an :any:`ldapserver.exceptions.LDAPError` instance in a handler method
+of :any:`ldapserver.LDAPRequestHandler` aborts the request processing and sends
+an appropriate response message with the corresponding result code and (if any)
+the diagnostic message.
 
 .. autoexception:: ldapserver.exceptions.LDAPError
 .. autoexception:: ldapserver.exceptions.LDAPOperationsError
diff --git a/ldapserver/dn.py b/ldapserver/dn.py
index eb90cbc..b6ce3bf 100644
--- a/ldapserver/dn.py
+++ b/ldapserver/dn.py
@@ -22,7 +22,8 @@ from . import exceptions
 __all__ = ['DN', 'RDN', 'RDNAssertion', 'DNWithUID']
 
 class DN(tuple):
-	'''Distinguished Name consiting of zero ore more `RDN` objects'''
+	'''Distinguished Name consisting of zero ore more :class:`RDN` objects'''
+	#:
 	schema: typing.Any
 
 	def __new__(cls, schema, *args, **kwargs):
@@ -45,6 +46,15 @@ class DN(tuple):
 
 	@classmethod
 	def from_str(cls, schema, expr):
+		'''Parse string representation of a DN according to RFC 4514
+
+		:param schema: Schema for the DN
+		:type schema: schema.Schema
+		:param expr: DN string representation
+		:type expr: str
+		:raises ValueError: if expr is invalid
+		:returns: Parsed DN
+		:rtype: DN'''
 		escaped = False
 		rdns = []
 		token = ''
@@ -64,10 +74,14 @@ class DN(tuple):
 		return cls(schema, *rdns)
 
 	def __str__(self):
+		'''Return string representation of DN according to RFC 4514
+
+		:returns: Representation of self
+		:rtype: str'''
 		return ','.join(map(str, self))
 
 	def __repr__(self):
-		return '<ldapserver.dn.DN %s>'%str(self)
+		return '<ldapserver.DN %s>'%str(self)
 
 	def __eq__(self, obj):
 		return type(self) is type(obj) and super().__eq__(obj)
@@ -97,38 +111,85 @@ class DN(tuple):
 		return self[:-minlen or None], value[:-minlen or None]
 
 	def is_direct_child_of(self, base):
+		'''Return whether self is a direct child of base
+
+		:param base: parent DN
+		:type base: DN
+		:rtype: bool
+
+		Example:
+
+			>>> schema = schema.RFC4519_SCHEMA
+			>>> dn1 = DN(schema, 'uid=jsmith,dc=example,dc=net')
+			>>> dn2 = DN(schema, 'dc=example,dc=net')
+			>>> dn3 = DN(schema, 'dc=net')
+			>>> dn1.is_direct_child_of(dn2)
+			True
+			>>> dn1.is_direct_child_of(dn1) or dn1.is_direct_child_of(dn3)
+			False
+
+		'''
 		rchild, rbase = self.__strip_common_suffix(DN(self.schema, base))
 		return not rbase and len(rchild) == 1
 
 	def in_subtree_of(self, base):
+		'''Return whether self is in the subtree of base
+
+		:param base: parent DN
+		:type base: DN
+		:rtype: bool
+
+		Example:
+
+			>>> schema = schema.RFC4519_SCHEMA
+			>>> dn1 = DN(schema, 'uid=jsmith,dc=example,dc=net')
+			>>> dn2 = DN(schema, 'dc=example,dc=net')
+			>>> dn3 = DN(schema, 'dc=net')
+			>>> dn1.in_subtree_of(dn1) and dn1.in_subtree_of(dn2) and dn1.in_subtree_of(dn3)
+			True
+			>>> dn2.in_subtree_of(dn1)
+			False
+
+		'''
 		rchild, rbase = self.__strip_common_suffix(DN(self.schema, base)) # pylint: disable=unused-variable
 		return not rbase
 
 	@property
 	def object_attribute(self):
+		'''Attribute name of the first RDN. None if there are no RDNs or if the
+		first RDN consists of more than one assertion.'''
 		if len(self) == 0:
 			return None
 		return self[0].attribute # pylint: disable=no-member
 
 	@property
 	def object_attribute_type(self):
+		''':any:`schema.AttributeType` of the first RDN. None if there are no RDNs
+		or if the first RDN consists of more than one assertion.'''
 		if len(self) == 0:
 			return None
 		return self[0].attribute_type # pylint: disable=no-member
 
 	@property
 	def object_value(self):
+		'''Attribute value of the first RDN. None if there are no RDNs or if the
+		first RDN consists of more than one assertion. Type of value depends on
+		the syntax of the attribute type.'''
 		if len(self) == 0:
 			return None
 		return self[0].value # pylint: disable=no-member
 
-	@property
-	def object_value_normalized(self):
-		if len(self) == 0:
-			return None
-		return self[0].value_normalized # pylint: disable=no-member
-
 class DNWithUID(DN):
+	'''Distinguished Name with an optional bit string
+
+	Used to represent values of the "Name and Optional UID" syntax (see RFC4517)
+	This syntax is used for e.g. the "uniqueMember" attribute type (RFC4519).
+
+	If created without the optional bit string (uid) part, a regular DN object is
+	returned.'''
+	#:
+	schema: typing.Any
+
 	# pylint: disable=arguments-differ,no-member
 	def __new__(cls, schema, dn, uid=None):
 		if not uid:
@@ -141,24 +202,42 @@ class DNWithUID(DN):
 
 	@classmethod
 	def from_str(cls, schema, expr):
+		'''Parse string representation
+
+		:param schema: Schema
+		:type schema: schema.Schema
+		:param expr: DN string representation with optional ``#``-separated bit
+		             string part (e.g. ``0b1010``)
+		:type expr: str
+		:raises ValueError: if expr is invalid
+		:returns: :any:`DNWithUID` if expr includes the optional bit string part,
+		          :any:`DN` otherwise
+		:rtype: DN or DNWithUID
+		'''
 		dn_part, uid_part = (expr.rsplit('#', 1) + [''])[:2]
 		return cls(schema, DN.from_str(schema, dn_part), uid_part or None)
 
 	def __str__(self):
+		'''Return string representation
+
+		:returns: Representation of self
+		:rtype: str'''
 		return super().__str__() + '#' + self.uid
 
 	def __repr__(self):
-		return '<ldapserver.dn.DNWithUID %s>'%str(self)
+		return '<ldapserver.DNWithUID %s>'%str(self)
 
 	def __eq__(self, obj):
 		return type(self) is type(obj) and super().__eq__(obj) and self.uid == obj.uid
 
 	@property
 	def dn(self):
+		'''Return DN part as a regular :any:`DN` object'''
 		return DN(self.schema, *self)
 
 class RDN(tuple):
-	'''Relative Distinguished Name consisting of one or more `RDNAssertion` objects'''
+	'''Relative Distinguished Name consisting of one or more :class:`RDNAssertion` objects'''
+	#:
 	schema: typing.Any
 
 	def __new__(cls, schema, *assertions, **kwargs):
@@ -181,6 +260,15 @@ class RDN(tuple):
 
 	@classmethod
 	def from_str(cls, schema, expr):
+		'''Parse string representation of an RDN according to RFC 4514
+
+		:param schema: Schema for the RDN
+		:type schema: schema.Schema
+		:param expr: RDN string representation
+		:type expr: str
+		:raises ValueError: if expr is invalid
+		:returns: Parsed RDN
+		:rtype: RDN'''
 		escaped = False
 		assertions = []
 		token = ''
@@ -200,10 +288,20 @@ class RDN(tuple):
 		return cls(schema, *assertions)
 
 	def __str__(self):
+		'''Return string representation of RDN according to RFC 4514
+
+		:returns: Representation of self
+		:rtype: str
+
+		Example:
+
+			>>> str(RDN(ou='Sales', cn='J.  Smith'))
+			'ou=Sales+cn=J.  Smith'
+		'''
 		return '+'.join(map(str, self))
 
 	def __repr__(self):
-		return '<ldapserver.dn.RDN %s>'%str(self)
+		return '<ldapserver.RDN %s>'%str(self)
 
 	def __eq__(self, obj):
 		return type(self) is type(obj) and set(self) == set(obj)
@@ -221,18 +319,25 @@ class RDN(tuple):
 
 	@property
 	def attribute(self):
+		'''Attribute name of the contained assertion. None if the RDN consists of
+		more than one assertion.'''
 		if len(self) != 1:
 			return None
 		return self[0].attribute
 
 	@property
 	def attribute_type(self):
+		'''Attribute type of the contained assertion. None if the RDN consists of
+		more than one assertion.'''
 		if len(self) != 1:
 			return None
 		return self[0].attribute_type
 
 	@property
 	def value(self):
+		'''Attribute value of the contained assertion. None if the RDN consists of
+		more than one assertion. Type of value depends on the syntax of the
+		attribute type.'''
 		if len(self) != 1:
 			return None
 		return self[0].value
@@ -276,11 +381,14 @@ HEXDIGITS = (
 )
 
 class RDNAssertion:
-	'''A single attribute value assertion'''
-	__slots__ = ['attribute', 'attribute_type', 'value']
+	__slots__ = ['attribute', 'attribute_type', 'value', 'schema']
+	#: Attribute name
 	attribute: str
+	#: :class:`schema.AttributeType`
 	attribute_type: typing.Any
+	#: Attribute value (type depends on the syntax of the attribute type)
 	value: typing.Any
+	#:
 	schema: typing.Any
 
 	def __init__(self, schema, attribute, value):
@@ -326,6 +434,15 @@ class RDNAssertion:
 
 	@classmethod
 	def from_str(cls, schema, expr):
+		'''Parse string representation of an RDN assertion according to RFC 4514
+
+		:param schema: Schema for the RDN assertion
+		:type schema: schema.Schema
+		:param expr: RDN assertion string representation
+		:type expr: str
+		:raises ValueError: if expr is invalid
+		:returns: Parsed assertion
+		:rtype: RDNAssertion'''
 		attribute, escaped_value = expr.split('=', 1)
 		if escaped_value.startswith('#'):
 			# The "#..." form is used for unknown attribute types and those without
@@ -363,6 +480,10 @@ class RDNAssertion:
 		return cls(schema, attribute, value)
 
 	def __str__(self):
+		'''Return string representation of the RDN assertion according to RFC 4514
+
+		:returns: Representation of self
+		:rtype: str'''
 		encoded_value = self.attribute_type.syntax.encode(self.value)
 		escaped_value = ''
 		for index in range(len(encoded_value)):
@@ -383,7 +504,7 @@ class RDNAssertion:
 		return hash(self.attribute_type.oid)
 
 	def __repr__(self):
-		return '<ldapserver.dn.RDNAssertion %s>'%str(self)
+		return '<ldapserver.RDNAssertion %s>'%str(self)
 
 	def __eq__(self, obj):
 		return type(self) is type(obj) and self.attribute_type is obj.attribute_type and \
diff --git a/ldapserver/exceptions.py b/ldapserver/exceptions.py
index 109e310..b10a6a6 100644
--- a/ldapserver/exceptions.py
+++ b/ldapserver/exceptions.py
@@ -14,15 +14,22 @@ class LDAPError(Exception):
 #	RESULT_CODE = LDAPResultCode.success
 
 class LDAPOperationsError(LDAPError):
+	'''Indicates that the operation is not properly sequenced with relation to
+	other operations (of same or different type). (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.operationsError
 
 class LDAPProtocolError(LDAPError):
+	'''Indicates the server received data that is not well-formed. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.protocolError
 
 class LDAPTimeLimitExceeded(LDAPError):
+	'''Indicates that the time limit specified by the client was
+	exceeded before the operation could be completed. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.timeLimitExceeded
 
 class LDAPSizeLimitExceeded(LDAPError):
+	'''Indicates that the size limit specified by the client was
+	exceeded before the operation could be completed. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.sizeLimitExceeded
 
 #class LDAPCompareFalse(LDAPError):
@@ -32,97 +39,160 @@ class LDAPSizeLimitExceeded(LDAPError):
 #	RESULT_CODE = LDAPResultCode.compareTrue
 
 class LDAPAuthMethodNotSupported(LDAPError):
+	'''Indicates that the authentication method or mechanism is not
+	supported. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.authMethodNotSupported
 
 class LDAPStrongerAuthRequired(LDAPError):
+	'''Indicates the server requires strong(er) authentication in
+	order to complete the operation. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.strongerAuthRequired
 
 #class LDAPReferral(LDAPError):
 #	RESULT_CODE = LDAPResultCode.referral
 
 class LDAPAdminLimitExceeded(LDAPError):
+	'''Indicates that an administrative limit has been exceeded. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.adminLimitExceeded
 
 class LDAPUnavailableCriticalExtension(LDAPError):
+	'''Indicates a critical control is unrecognized. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.unavailableCriticalExtension
 
 class LDAPConfidentialityRequired(LDAPError):
+	'''Indicates that data confidentiality protections are required. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.confidentialityRequired
 
 #class LDAPSaslBindInProgress(LDAPError):
 #	RESULT_CODE = LDAPResultCode.saslBindInProgress
 
 class LDAPNoSuchAttribute(LDAPError):
+	'''Indicates that the named entry does not contain the specified
+	attribute or attribute value. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.noSuchAttribute
 
 class LDAPUndefinedAttributeType(LDAPError):
+	'''Indicates that a request field contains an unrecognized
+	attribute description. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.undefinedAttributeType
 
 class LDAPInappropriateMatching(LDAPError):
+	'''Indicates that an attempt was made (e.g., in an assertion) to
+	use a matching rule not defined for the attribute type
+	concerned. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.inappropriateMatching
 
 class LDAPConstraintViolation(LDAPError):
+	'''Indicates that the client supplied an attribute value that
+	does not conform to the constraints placed upon it by the
+	data model. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.constraintViolation
 
 class LDAPAttributeOrValueExists(LDAPError):
+	'''Indicates that the client supplied an attribute or value to
+	be added to an entry, but the attribute or value already
+	exists. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.attributeOrValueExists
 
 class LDAPInvalidAttributeSyntax(LDAPError):
+	''' Indicates that a purported attribute value does not conform
+ to the syntax of the attribute. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.invalidAttributeSyntax
 
 class LDAPNoSuchObject(LDAPError):
+	'''Indicates that the object does not exist in the DIT. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.noSuchObject
 
 class LDAPAliasProblem(LDAPError):
+	'''Indicates that an alias problem has occurred. For example,
+	the code may used to indicate an alias has been dereferenced
+	that names no object. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.aliasProblem
 
 class LDAPInvalidDNSyntax(LDAPError):
+	'''Indicates that an LDAPDN or RelativeLDAPDN field (e.g., search
+	base, target entry, ModifyDN newrdn, etc.) of a request does
+	not conform to the required syntax or contains attribute
+	values that do not conform to the syntax of the attribute's
+	type. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.invalidDNSyntax
 
 class LDAPAliasDereferencingProblem(LDAPError):
+	'''Indicates that a problem occurred while dereferencing an
+	alias.  Typically, an alias was encountered in a situation
+	where it was not allowed or where access was denied. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.aliasDereferencingProblem
 
 class LDAPInappropriateAuthentication(LDAPError):
+	'''Indicates the server requires the client that had attempted
+	to bind anonymously or without supplying credentials to
+	provide some form of credentials. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.inappropriateAuthentication
 
 class LDAPInvalidCredentials(LDAPError):
+	'''Indicates that the provided credentials (e.g., the user's name
+	and password) are invalid. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.invalidCredentials
 
 class LDAPInsufficientAccessRights(LDAPError):
+	'''Indicates that the client does not have sufficient access
+	rights to perform the operation. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.insufficientAccessRights
 
 class LDAPBusy(LDAPError):
+	'''Indicates that the server is too busy to service the
+	operation. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.busy
 
 class LDAPUnavailable(LDAPError):
+	'''Indicates that the server is shutting down or a subsystem
+	necessary to complete the operation is offline. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.unavailable
 
 class LDAPUnwillingToPerform(LDAPError):
+	'''Indicates that the server is unwilling to perform the
+	operation. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.unwillingToPerform
 
 class LDAPLoopDetect(LDAPError):
+	'''Indicates that the server has detected an internal loop (e.g.,
+	while dereferencing aliases or chaining an operation). (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.loopDetect
 
 class LDAPNamingViolation(LDAPError):
+	'''Indicates that the entry's name violates naming restrictions. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.namingViolation
 
 class LDAPObjectClassViolation(LDAPError):
+	'''Indicates that the entry violates object class restrictions. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.objectClassViolation
 
 class LDAPNotAllowedOnNonLeaf(LDAPError):
+	'''Indicates that the operation is inappropriately acting upon a
+	non-leaf entry. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.notAllowedOnNonLeaf
 
 class LDAPNotAllowedOnRDN(LDAPError):
+	'''Indicates that the operation is inappropriately attempting to
+	remove a value that forms the entry's relative distinguished
+	name. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.notAllowedOnRDN
 
 class LDAPEntryAlreadyExists(LDAPError):
+	'''Indicates that the request cannot be fulfilled (added, moved,
+	or renamed) as the target entry already exists. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.entryAlreadyExists
 
 class LDAPObjectClassModsProhibited(LDAPError):
+	'''Indicates that an attempt to modify the object class(es) of
+	an entry's 'objectClass' attribute is prohibited. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.objectClassModsProhibited
 
 class LDAPAffectsMultipleDSAs(LDAPError):
+	'''Indicates that the operation cannot be performed as it would
+	affect multiple servers (DSAs). (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.affectsMultipleDSAs
 
 class LDAPOther(LDAPError):
+	'''Indicates the server has encountered an internal error. (RFC 4511)'''
 	RESULT_CODE = LDAPResultCode.other
diff --git a/ldapserver/ldap.py b/ldapserver/ldap.py
index 8fdb990..980c3ac 100644
--- a/ldapserver/ldap.py
+++ b/ldapserver/ldap.py
@@ -39,26 +39,23 @@ def escape_filter_assertionvalue(value):
 	return bytes(res).decode()
 
 class Filter(asn1.Choice, ABC):
-	'''Base class for filters in SEARCH operations'''
+	'''Base class for filters in SEARCH operations
+
+	All subclasses implement ``__str__`` according to RFC4515 "String
+	Representation of Search Filters".'''
 
 	@abstractmethod
 	def __str__(self):
 		raise NotImplementedError()
 
 class FilterAnd(asn1.Wrapper, Filter):
-	'''AND conjunction of multiple filters ``(&filters...)``
-
-	.. py:attribute:: filters
-		:type: list
-		:value: []
-
-		List of :any:`Filter` objects
-	'''
+	'''AND conjunction of multiple filters ``(&filters...)``'''
 	BER_TAG = (2, True, 0)
 	WRAPPED_ATTRIBUTE = 'filters'
 	WRAPPED_TYPE = asn1.Set
 	WRAPPED_CLSATTRS = {'SET_TYPE': Filter}
 
+	#:
 	filters: typing.List[Filter]
 
 	def __init__(self, filters=None):
@@ -68,19 +65,13 @@ class FilterAnd(asn1.Wrapper, Filter):
 		return '(&%s)'%(''.join([str(subfilter) for subfilter in self.filters]))
 
 class FilterOr(asn1.Wrapper, Filter):
-	'''OR conjunction of multiple filters ``(|filters...)``
-
-	.. py:attribute:: filters
-		:type: list
-		:value: []
-
-		List of :any:`Filter` objects
-	'''
+	'''OR conjunction of multiple filters ``(|filters...)``'''
 	BER_TAG = (2, True, 1)
 	WRAPPED_ATTRIBUTE = 'filters'
 	WRAPPED_TYPE = asn1.Set
 	WRAPPED_CLSATTRS = {'SET_TYPE': Filter}
 
+	#:
 	filters: typing.List[Filter]
 
 	def __init__(self, filters=None):
@@ -90,17 +81,14 @@ class FilterOr(asn1.Wrapper, Filter):
 		return '(|%s)'%(''.join([str(subfilter) for subfilter in self.filters]))
 
 class FilterNot(asn1.Sequence, Filter):
-	'''Negation of a filter ``(!filter)``
-
-	.. py:attribute:: filter
-		:type: Filter
-	'''
+	'''Negation of a filter ``(!filter)``'''
 
 	BER_TAG = (2, True, 2)
 	SEQUENCE_FIELDS = [
 		(Filter, 'filter', None, False)
 	]
 
+	#:
 	filter: Filter
 
 	# pylint: disable=redefined-builtin
@@ -111,20 +99,16 @@ class FilterNot(asn1.Sequence, Filter):
 		return '(!%s)'%str(self.filter)
 
 class FilterEqual(asn1.Sequence, Filter):
-	'''Attribute equal filter ``(attribute=value)``
-
-	.. py:attribute:: attribute
-		:type: str
-	.. py:attribute:: value
-		:type: bytes
-	'''
+	'''Attribute equal filter ``(attribute=value)``'''
 	BER_TAG = (2, True, 3)
 	SEQUENCE_FIELDS = [
 		(LDAPString, 'attribute', None, False),
 		(asn1.OctetString, 'value', None, False)
 	]
 
+	#:
 	attribute: str
+	#:
 	value: bytes
 
 	def __init__(self, attribute=None, value=None):
@@ -161,29 +145,20 @@ class Substrings(asn1.SequenceOf):
 	SET_TYPE = Substring
 
 class FilterSubstrings(asn1.Sequence, Filter):
-	'''Attribute substrings filter ``(attribute=initial*any*final)``
-
-	.. py:attribute:: attribute
-		:type: str
-	.. py:attribute:: inital
-		:type: bytes
-	.. py:attribute:: any
-		:type: bytes
-	'''
+	'''Attribute substrings filter ``(attribute=initial*any1*any2*final)``'''
 	BER_TAG = (2, True, 4)
 	SEQUENCE_FIELDS = [
 		(LDAPString, 'attribute', None, False),
 		(Substrings, 'substrings', lambda: [], False)
 	]
 
+	#:
 	attribute: str
 	substrings: typing.List[Substring]
 
-	def __init__(self, attribute=None, substrings=None):
-		super().__init__(attribute=attribute, substrings=substrings)
-
 	@property
 	def initial_substring(self):
+		'''Initial substring (:any:`bytes`). None if there is no initial substring or if the filter is invalid.'''
 		results = [substring.value for substring in self.substrings if isinstance(substring, InitialSubstring)]
 		if len(results) != 1:
 			return None
@@ -191,10 +166,12 @@ class FilterSubstrings(asn1.Sequence, Filter):
 
 	@property
 	def any_substrings(self):
+		'''List of "any" substrings (list of :any:`bytes`, may be empty)'''
 		return [substring.value for substring in self.substrings if isinstance(substring, AnySubstring)]
 
 	@property
 	def final_substring(self):
+		'''Final substring (:any:`bytes`). None if there is no final substring or if the filter is invalid.'''
 		results = [substring.value for substring in self.substrings if isinstance(substring, FinalSubstring)]
 		if len(results) != 1:
 			return None
@@ -206,6 +183,7 @@ class FilterSubstrings(asn1.Sequence, Filter):
 		return f'({self.attribute}={value})'
 
 class FilterGreaterOrEqual(asn1.Sequence, Filter):
+	'''Attribute greater or equal filter ``(attribute>=value)``'''
 	BER_TAG = (2, True, 5)
 	SEQUENCE_FIELDS = [
 		(LDAPString, 'attribute', None, False),
@@ -222,13 +200,16 @@ class FilterGreaterOrEqual(asn1.Sequence, Filter):
 		return '(%s>=%s)'%(self.attribute, escape_filter_assertionvalue(self.value))
 
 class FilterLessOrEqual(asn1.Sequence, Filter):
+	'''Attribute less or equal filter ``(attribute<=value)``'''
 	BER_TAG = (2, True, 6)
 	SEQUENCE_FIELDS = [
 		(LDAPString, 'attribute', None, False),
 		(asn1.OctetString, 'value', None, False)
 	]
 
+	#:
 	attribute: str
+	#:
 	value: bytes
 
 	def __init__(self, attribute=None, value=None):
@@ -238,16 +219,13 @@ class FilterLessOrEqual(asn1.Sequence, Filter):
 		return '(%s<=%s)'%(self.attribute, escape_filter_assertionvalue(self.value))
 
 class FilterPresent(asn1.Wrapper, Filter):
-	'''Attribute present filter ``(attribute=*)``
-
-	.. py:attribute:: attribute
-		:type: str
-	'''
+	'''Attribute present filter ``(attribute=*)``'''
 	BER_TAG = (2, False, 7)
 	WRAPPED_ATTRIBUTE = 'attribute'
 	WRAPPED_TYPE = LDAPString
 	WRAPPED_DEFAULT = None
 
+	#:
 	attribute: str
 
 	def __init__(self, attribute=None):
@@ -257,13 +235,16 @@ class FilterPresent(asn1.Wrapper, Filter):
 		return '(%s=*)'%(self.attribute)
 
 class FilterApproxMatch(asn1.Sequence, Filter):
+	'''Attribute approximately equal filter ``(attribute~=value)``'''
 	BER_TAG = (2, True, 8)
 	SEQUENCE_FIELDS = [
 		(LDAPString, 'attribute', None, False),
 		(asn1.OctetString, 'value', None, False)
 	]
 
+	#:
 	attribute: str
+	#:
 	value: bytes
 
 	def __init__(self, attribute=None, value=None):
@@ -273,6 +254,7 @@ class FilterApproxMatch(asn1.Sequence, Filter):
 		return '(%s~=%s)'%(self.attribute, escape_filter_assertionvalue(self.value))
 
 class FilterExtensibleMatch(asn1.Sequence, Filter):
+	'''Extensible match filter ``(attribute:caseExactMatch:=value)``'''
 	BER_TAG = (2, True, 9)
 	SEQUENCE_FIELDS = [
 		(asn1.retag(LDAPString, (2, False, 1)), 'matchingRule', None, True),
@@ -281,9 +263,15 @@ class FilterExtensibleMatch(asn1.Sequence, Filter):
 		(asn1.retag(asn1.Boolean, (2, False, 4)), 'dnAttributes', None, True),
 	]
 
+	#: Matching rule OID or short descriptive name (optional, str or None)
 	matchingRule: str
+	#: Attribute type OID or short descriptive name (optional, str or None)
 	type: str
+	#: Assertion value (bytes, encoded with LDAP-specific encoding according to matching rule syntax)
 	matchValue: bytes
+	#: Apply matching to all RDN assertions in addition to attribute values
+	#: (e.g. ``(dc:dn:=example)`` matches ``cn=test,dc=example,dc=com`` even
+	#: if it has no ``dc`` attribute, because ``dc=example`` is part of its DN)
 	dnAttributes: bool
 
 	def __str__(self):
@@ -412,7 +400,9 @@ class PartialAttribute(asn1.Sequence):
 		(AttributeValueSet, 'vals', lambda: [], False),
 	]
 
+	#:
 	type: str
+	#:
 	vals: typing.List[bytes]
 
 class PartialAttributeList(asn1.SequenceOf):
@@ -496,7 +486,9 @@ class SearchResultEntry(asn1.Sequence, ProtocolOp):
 		(PartialAttributeList, 'attributes', lambda: [], False),
 	]
 
+	#:
 	objectName: str
+	#:
 	attributes: typing.List[PartialAttribute]
 
 class SearchResultDone(LDAPResult, ProtocolOp):
diff --git a/ldapserver/objects.py b/ldapserver/objects.py
index c568470..a8c0a55 100644
--- a/ldapserver/objects.py
+++ b/ldapserver/objects.py
@@ -47,6 +47,7 @@ class AttributeDict(collections.abc.MutableMapping):
 	conform to the attribute's syntax.'''
 	def __init__(self, schema, **attributes):
 		self.__attributes = {}
+		#:
 		self.schema = schema
 		for key, values in attributes.items():
 			self[key] = values
@@ -112,6 +113,7 @@ class FilterResult(enum.Enum):
 class Object(AttributeDict):
 	def __init__(self, schema, dn, **attributes):
 		super().__init__(schema, **attributes)
+		#: Objects distinguished name (:class:`DN`)
 		self.dn = DN(schema, dn)
 
 	def __search_match_dn(self, basedn, scope):
@@ -218,6 +220,25 @@ class Object(AttributeDict):
 		       self.__search_match_filter(filter_obj) == FilterResult.TRUE
 
 	def search(self, base_obj, scope, filter_obj, attributes, types_only):
+		'''Return SEARCH result for the object if it matches the operation
+		parameters
+
+		:param base_obj: DN of the base object
+		:type base_obj: str
+		:param scope: Scope of base_obj
+		:type scope: ldap.SearchScope
+		:param filter_obj: Search filter
+		:type filter_obj: ldap.Filter
+		:param attributes: Requested attributes
+		:type attributes: list of str
+		:param types_only: Omit values in :class:`ldap.PartialAttribute`
+		:type types_only: bool
+		:returns: SEARCH result for the object if it matches the operation
+		          parameters, None otherwise
+		:rtype: ldap.SearchResultEntry or None
+
+		Conforms to RFC4511 and RFC4526 (empty AND/OR filters are supported and
+		treated as absolute TRUE/FALSE).'''
 		if not self.match_search(base_obj, scope, filter_obj):
 			return None
 		selected_attributes = set()
@@ -238,6 +259,22 @@ class Object(AttributeDict):
 		return ldap.SearchResultEntry(str(self.dn), partial_attributes)
 
 	def compare(self, dn, attribute, value):
+		'''Return the result of the COMPARE operation applied to the object
+
+		:param dn: DN of the object to be compared
+		:type dn: str
+		:param attribute: Attribute OID or short descriptive name
+		:type attribute: str
+		:param value: Assertion value
+		:type value: bytes
+		:raises exceptions.LDAPNoSuchObject: if dn does not refer to the object
+		:raises exceptions.LDAPError: if operation results to anything other than
+		                              TRUE/FALSE
+		:return: True/False if COMPARE operation evaluates to TRUE/FALSE
+		:rtype: bool
+
+		Evaluation is essentially applying the EQUALITY matching rule of attribute
+		on the values of attribute with the assertion value.'''
 		try:
 			dn = DN.from_str(self.schema, dn)
 		except ValueError as exc:
@@ -251,8 +288,8 @@ class Object(AttributeDict):
 		return attribute_type.match_equal(self.get(attribute_type, subtypes=True), value)
 
 class RootDSE(Object):
-	def __init__(self, schema, *args, **kwargs):
-		super().__init__(schema, DN(schema), *args, **kwargs)
+	def __init__(self, schema, **attributes):
+		super().__init__(schema, DN(schema), **attributes)
 		self.setdefault('objectClass', ['top'])
 
 	def match_search(self, base_obj, scope, filter_obj):
@@ -263,6 +300,7 @@ class RootDSE(Object):
 class WildcardValue:
 	pass
 
+#: Special wildcard value for :class:`ObjectTemplate`
 WILDCARD_VALUE = WildcardValue()
 
 class TemplateFilterResult(enum.Enum):
@@ -429,18 +467,54 @@ class ObjectTemplate(AttributeDict):
 		return AttributeDict(self.schema)
 
 	def match_search(self, base_obj, scope, filter_obj):
-		'''Return whether objects based on this template might match the search parameters'''
+		'''Return whether objects based on this template might match the search parameters
+
+		:param base_obj: DN of the base object
+		:type base_obj: str
+		:param scope: Scope of base_obj
+		:type scope: ldap.SearchScope
+		:param filter_obj: Search filter
+		:type filter_obj: ldap.Filter
+		:returns: True if objects based on this template might match the SEARCH
+		          parameters or False if they do not match
+		:rtype: bool'''
 		return self.__search_match_dn(DN.from_str(self.schema, base_obj), scope) and \
 		       self.__search_match_filter(filter_obj) in (TemplateFilterResult.TRUE,
 		                                                  TemplateFilterResult.MAYBE_TRUE)
 
 	def extract_search_constraints(self, base_obj, scope, filter_obj):
+		'''Return approximate value constraints for objects that match SEARCH parameters
+
+		:returns: :class:`AttributeDict` with values that any object must have
+		          to match potentially match SEARCH parameters
+		:rtype: AttributeDict
+
+		Example:
+
+			>>> subschema = SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema')
+			>>> template = subschema.ObjectTemplate('ou=users,dc=example,dc=com', 'cn', objectclass=['person', 'top'], sn=[WILDCARD_VALUE])
+			>>> template.extract_search_constraints('cn=test,ou=users,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterEqual('sn', b'foobar'))
+			subschema.AttributeDict(cn=['test'], sn=['foobar'])
+
+		Note that the results are an approximation (i.e. some constrains may be
+		missing) and the quality may improve over time. Also note that every object
+		that matches the SEARCH parameters also matches the constrains, but not
+		vice versa.'''
 		constraints = self.__extract_filter_constraints(filter_obj)
 		for key, values in self.__extract_dn_constraints(DN.from_str(self.schema, base_obj), scope).items():
 			constraints[key] += values
 		return constraints
 
 	def create_object(self, rdn_value, **attributes):
+		'''Instanciate :class:`Object` based on template
+
+		:param rdn_value: RDN value for DN construction
+		:type rdn_value: any
+		:rtype: Object
+
+		Template attributes set to :any:`WILDCARD_VALUE` are stripped. Only
+		template attributes set to :any:`WILDCARD_VALUE` may be overwritten
+		with `attributes`.'''
 		obj = Object(self.schema, DN(self.schema, self.parent_dn, **{self.rdn_attribute: rdn_value}))
 		for key, values in attributes.items():
 			if WILDCARD_VALUE not in self[key]:
@@ -452,7 +526,7 @@ class ObjectTemplate(AttributeDict):
 		return obj
 
 class SubschemaSubentry(Object):
-	'''Special :any:`Object` providing information on a Schema'''
+	'''Special :class:`Object` providing information on a Schema'''
 	def __init__(self, schema, dn, **attributes):
 		super().__init__(schema, dn, **attributes)
 		self['subschemaSubentry'] = [self.dn]
@@ -464,9 +538,13 @@ class SubschemaSubentry(Object):
 		self['attributeTypes'] = schema.attribute_type_definitions
 		self['matchingRuleUse'] = schema.matching_rule_use_definitions
 		# pylint: disable=invalid-name
+		#: Shorthand for :class:`AttributeDict`
 		self.AttributeDict = lambda **attributes: AttributeDict(schema, **attributes)
+		#: Shorthand for :class:`Object`
 		self.Object = lambda *args, **attributes: Object(schema, *args, subschemaSubentry=[self.dn], **attributes)
+		#: Shorthand for :class:`RootDSE`
 		self.RootDSE = lambda **attributes: RootDSE(schema, subschemaSubentry=[self.dn], **attributes)
+		#: Shorthand for :class:`ObjectTemplate`
 		self.ObjectTemplate = lambda *args, **kwargs: ObjectTemplate(schema, *args, subschemaSubentry=[self.dn], **kwargs)
 		class Wrapper:
 			def __init__(self, cls, schema):
@@ -479,8 +557,11 @@ class SubschemaSubentry(Object):
 			def from_str(self, *args, **kwargs):
 				return self.cls.from_str(self.schema, *args, **kwargs)
 
+		#: Shorthand for :class:`DN`
 		self.DN = Wrapper(DN, schema)
+		#: Shorthand for :class:`RDN`
 		self.RDN = Wrapper(RDN, schema)
+		#: Shorthand for :class:`RDNAssertion`
 		self.RDNAssertion = Wrapper(RDNAssertion, schema)
 
 	def match_search(self, base_obj, scope, filter_obj):
diff --git a/ldapserver/schema/__init__.py b/ldapserver/schema/__init__.py
index 8dda82c..908e128 100644
--- a/ldapserver/schema/__init__.py
+++ b/ldapserver/schema/__init__.py
@@ -1,4 +1,4 @@
-from .types import Schema
+from .types import *
 from .definitions import *
 from . import syntaxes, matching_rules
 
@@ -34,7 +34,10 @@ RFC4512_OBJECT_CLASSES = [
 	"( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' SUP top AUXILIARY )",
 	"( 2.5.20.1 NAME 'subschema' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )",
 ]
-CORE_SCHEMA = RFC4512_SCHEMA = Schema(syntax_definitions=syntaxes.ALL, matching_rule_definitions=matching_rules.ALL, attribute_type_definitions=RFC4512_ATTRIBUTE_TYPES, object_class_definitions=RFC4512_OBJECT_CLASSES)
+#:
+RFC4512_SCHEMA = Schema(syntax_definitions=syntaxes.ALL, matching_rule_definitions=matching_rules.ALL, attribute_type_definitions=RFC4512_ATTRIBUTE_TYPES, object_class_definitions=RFC4512_OBJECT_CLASSES)
+#:
+CORE_SCHEMA = RFC4512_SCHEMA
 
 RFC4519_ATTRIBUTE_TYPES = [
 	"( 2.5.4.15 NAME 'businessCategory' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
@@ -97,6 +100,7 @@ RFC4519_OBJECT_CLASSES = [
 	"( 2.5.6.10 NAME 'residentialPerson' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) )",
 	"( 1.3.6.1.1.3.1 NAME 'uidObject' SUP top AUXILIARY MUST uid )",
 ]
+#:
 RFC4519_SCHEMA = CORE_SCHEMA.extend(attribute_type_definitions=RFC4519_ATTRIBUTE_TYPES, object_class_definitions=RFC4519_OBJECT_CLASSES)
 
 RFC4523_ATTRIBUTE_TYPES = [
@@ -118,6 +122,7 @@ RFC4523_OBJECT_CLASSES = [
 	"( 2.5.6.16 NAME 'certificationAuthority' DESC 'X.509 certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList $ certificateRevocationList $ cACertificate ) MAY crossCertificatePair )",
 	"( 2.5.6.16.2 NAME 'certificationAuthority-V2' DESC 'X.509 certificate authority, version 2' SUP certificationAuthority AUXILIARY MAY deltaRevocationList )",
 ]
+#:
 RFC4523_SCHEMA = RFC4519_SCHEMA.extend(attribute_type_definitions=RFC4523_ATTRIBUTE_TYPES, object_class_definitions=RFC4523_OBJECT_CLASSES)
 
 RFC4524_ATTRIBUTE_TYPES = [
@@ -158,7 +163,10 @@ RFC4524_OBJECT_CLASSES = [
 	"( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST cn MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) )",
 	"( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' SUP top AUXILIARY MUST userPassword )",
 ]
-COSINE_SCHEMA = RFC4524_SCHEMA = RFC4519_SCHEMA.extend(attribute_type_definitions=RFC4524_ATTRIBUTE_TYPES, object_class_definitions=RFC4524_OBJECT_CLASSES)
+#:
+RFC4524_SCHEMA = RFC4519_SCHEMA.extend(attribute_type_definitions=RFC4524_ATTRIBUTE_TYPES, object_class_definitions=RFC4524_OBJECT_CLASSES)
+#:
+COSINE_SCHEMA = RFC4524_SCHEMA
 
 RFC3112_ATTRIBUTE_TYPES = [
 	"( 1.3.6.1.4.1.4203.1.3.3 NAME 'supportedAuthPasswordSchemes' DESC 'supported password storage schemes' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} USAGE dSAOperation )",
@@ -167,6 +175,7 @@ RFC3112_ATTRIBUTE_TYPES = [
 RFC3112_OBJECT_CLASSES = [
 	"( 1.3.6.1.4.1.4203.1.4.7 NAME 'authPasswordObject' DESC 'authentication password mix in class' AUXILIARY MAY authPassword )",
 ]
+#:
 RFC3112_SCHEMA = CORE_SCHEMA.extend(attribute_type_definitions=RFC3112_ATTRIBUTE_TYPES, object_class_definitions=RFC3112_OBJECT_CLASSES)
 
 RFC2079_ATTRIBUTE_TYPES = [
@@ -175,6 +184,8 @@ RFC2079_ATTRIBUTE_TYPES = [
 RFC2079_OBJECT_CLASSES = [
 	"( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'object that contains the URI attribute type' SUP top AUXILIARY MAY labeledURI )",
 ]
+#: :any:`Schema` implementing the labeledURIObject object class and the
+#: labeledURI attribute type.
 RFC2079_SCHEMA = CORE_SCHEMA.extend(attribute_type_definitions=RFC2079_ATTRIBUTE_TYPES, object_class_definitions=RFC2079_OBJECT_CLASSES)
 
 RFC2798_ATTRIBUTE_TYPES = [
@@ -196,7 +207,11 @@ RFC2798_ATTRIBUTE_TYPES = [
 RFC2798_OBJECT_CLASSES = [
 	"( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) )",
 ]
-INETORG_SCHMEA = RFC2798_SCHEMA = (RFC4524_SCHEMA|RFC2079_SCHEMA|RFC4523_SCHEMA).extend(attribute_type_definitions=RFC2798_ATTRIBUTE_TYPES, object_class_definitions=RFC2798_OBJECT_CLASSES)
+#: :any:`Schema` implementing the inetOrgPerson object class and its
+#: attribute types.
+RFC2798_SCHEMA = (RFC4524_SCHEMA|RFC2079_SCHEMA|RFC4523_SCHEMA).extend(attribute_type_definitions=RFC2798_ATTRIBUTE_TYPES, object_class_definitions=RFC2798_OBJECT_CLASSES)
+#:
+INETORG_SCHMEA = RFC2798_SCHEMA
 
 RFC2307BIS_ATTRIBUTE_TYPES = [
 	"( 1.3.6.1.1.1.1.0 NAME 'uidNumber' DESC 'An integer uniquely identifying a user in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
@@ -253,4 +268,5 @@ RFC2307BIS_OBJECT_CLASSES = [
 	"( 1.3.6.1.1.1.2.17 NAME 'automount' DESC 'Automount information' SUP top STRUCTURAL MUST ( automountKey $ automountInformation ) MAY description )",
 	"( 1.3.6.1.1.1.2.18 NAME 'groupOfMembers' DESC 'A group with members (DNs)' SUP top STRUCTURAL MUST cn MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description $ member ) )",
 ]
+#: :any:`Schema` implementing draft-howard-rfc2307bis-02 (updated/extended NIS schema)
 RFC2307BIS_SCHEMA = (RFC4524_SCHEMA|RFC3112_SCHEMA).extend(attribute_type_definitions=RFC2307BIS_ATTRIBUTE_TYPES, object_class_definitions=RFC2307BIS_OBJECT_CLASSES)
diff --git a/ldapserver/schema/definitions.py b/ldapserver/schema/definitions.py
index c52c745..3943cda 100644
--- a/ldapserver/schema/definitions.py
+++ b/ldapserver/schema/definitions.py
@@ -152,12 +152,25 @@ def parse_extensions(tokens):
 
 class SyntaxDefinition:
 	def __init__(self, oid, desc='', extensions=None, extra_compatability_tags=None):
+		#: Numeric OID (string)
 		self.oid = oid
+		#: Description (string, empty if there is none)
 		self.desc = desc
+		#:
 		self.extensions = extensions or {}
+		#: Set of compatability tags (strings, usually numeric OIDs). Syntaxes
+		#: always have at least their own numeric OID as a compatability tag.
+		#:
+		#: A matching rule can be applied to the values of a syntax if the matching
+		#: rules compatability tag is one of the syntaxes compatability tags.
 		self.compatability_tags = {oid} | set(extra_compatability_tags or tuple())
 
 	def __str__(self):
+		'''Return LDAP syntax definition string according to RFC4512
+
+		Example:
+
+			( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )'''
 		tokens = ['(', self.oid]
 		if self.desc:
 			tokens += ['DESC', qdstring_to_token(self.desc)]
@@ -166,6 +179,7 @@ class SyntaxDefinition:
 
 	@property
 	def first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch matching rule'''
 		return self.oid
 
 	def encode(self, schema, value):
@@ -194,8 +208,12 @@ class SyntaxDefinition:
 		raise exceptions.LDAPInvalidAttributeSyntax()
 
 class MatchingRuleKind(enum.Enum):
+	'''Values for :any:`MatchingRuleDefinition.kind`'''
+	#:
 	EQUALITY = enum.auto()
+	#:
 	ORDERING = enum.auto()
+	#:
 	SUBSTR = enum.auto()
 
 class MatchingRuleDefinition:
@@ -205,16 +223,33 @@ class MatchingRuleDefinition:
 			raise ValueError('syntax must be specified')
 		if not kind:
 			raise ValueError('kind must be specified')
+		#: Numeric OID (string)
 		self.oid = oid
+		#: OID of assertion value syntax (string)
 		self.syntax = syntax
+		#: Short descriptive names (list of strings, may be empty)
 		self.name = name or []
+		#: Description (string, empty if there is none)
 		self.desc = desc
+		#: bool
 		self.obsolete = obsolete
+		#:
 		self.extensions = extensions or {}
+		#: Compatability tag (string, usually a numeric OIDs). Defaults to the OID
+		#: of its syntax.
+		#:
+		#: A matching rule can be applied to the values of a syntax if the matching
+		#: rules compatability tag is one of the syntaxes compatability tags.
 		self.compatability_tag = compatability_tag or syntax
+		#:
 		self.kind = kind
 
 	def __str__(self):
+		'''Return matching rule definition string according to RFC4512
+
+		Example:
+
+			( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )'''
 		tokens = ['(', self.oid]
 		if self.name:
 			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
@@ -228,10 +263,11 @@ class MatchingRuleDefinition:
 
 	@property
 	def first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch matching rule'''
 		return self.oid
 
 	def match_equal(self, schema, attribute_values, assertion_value):
-		'''Return whether any attribute value equals assertion value
+		'''Return whether any attribute value equals the assertion value
 
 		Only available for EQUALITY matching rules.
 
@@ -241,7 +277,7 @@ class MatchingRuleDefinition:
 		raise exceptions.LDAPInappropriateMatching()
 
 	def match_approx(self, schema, attribute_values, assertion_value):
-		'''Return whether any attribute value approximatly equals assertion value
+		'''Return whether any attribute value approximatly equals the assertion value
 
 		Only available for EQUALITY matching rules.
 
@@ -271,7 +307,7 @@ class MatchingRuleDefinition:
 		raise exceptions.LDAPInappropriateMatching()
 
 	def match_substr(self, schema, attribute_values, inital_substring, any_substrings, final_substring):
-		'''Return whether any attribute value matches a substring assertion
+		'''Return whether any attribute value matches the substring assertion
 
 		Only available for SUBSTR matching rules.
 
@@ -295,14 +331,26 @@ class MatchingRuleDefinition:
 
 class MatchingRuleUseDefinition:
 	def __init__(self, oid, name=None, desc='', obsolete=False, applies=None, extensions=None):
+		#: Numeric OID (string)
 		self.oid = oid
+		#: Short descriptive names (list of strings, may be empty)
 		self.name = name or []
+		#: Description (string, empty if there is none)
 		self.desc = desc
+		#: bool
 		self.obsolete = obsolete
+		#: OIDs or short descriptive names of attribute types the matching rule can
+		#: be applied to (list of strings)
 		self.applies = applies or []
+		#:
 		self.extensions = extensions or {}
 
 	def __str__(self):
+		'''Return matching rule use definition string according to RFC4512
+
+		Example:
+
+			( 2.5.13.0 NAME 'objectIdentifierMatch' APPLIES ( supportedControl $ supportedExtension $ supportedFeatures $ supportedApplicationContext ) )'''
 		tokens = ['(', self.oid]
 		if self.name:
 			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
@@ -316,19 +364,20 @@ class MatchingRuleUseDefinition:
 
 	@property
 	def first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch matching rule'''
 		return self.oid
 
 
 class AttributeTypeUsage(enum.Enum):
-	'''Values for usage argument of `AttributeTypeUsage`'''
+	'''Values for :any:`AttributeTypeDefinition.usage`'''
 	# pylint: disable=invalid-name
-	# user
+	#: user (not an operational attribute)
 	userApplications = enum.auto()
-	# directory operational
+	#: directory operational
 	directoryOperation = enum.auto()
-	# DSA-shared operational
+	#: DSA-shared operational
 	distributedOperation = enum.auto()
-	# DSA-specific operational
+	#: DSA-specific operational
 	dSAOperation = enum.auto()
 
 class AttributeTypeDefinition:
@@ -340,23 +389,46 @@ class AttributeTypeDefinition:
 	             usage=AttributeTypeUsage.userApplications, extensions=None):
 		if not sup and not syntax:
 			raise ValueError('Either SUP or SYNTAX must be specified')
+		#: Numeric OID (string)
 		self.oid = oid
+		#: Short descriptive names (list of strings, may be empty)
 		self.name = name or []
+		#: Description (string, empty if there is none)
 		self.desc = desc
+		#: bool
 		self.obsolete = obsolete
+		#: OID or short descriptive name of superior attribute type (string or None)
 		self.sup = sup
+		#: OID or short descriptive name of equality matching rule (string or None)
 		self.equality = equality
+		#: OID or short descriptive name of ordering matching rule (string or None)
 		self.ordering = ordering
+		#: OID or short descriptive name of substrings matching rule (string or None)
 		self.substr = substr
+		#: OID of attribute value syntax (string or None)
 		self.syntax = syntax
+		#: Suggested minimum upper bound for attribute value length (int or None)
 		self.syntax_len = syntax_len
+		#: Whether attribute values are restricted to a single value (bool)
 		self.single_value = single_value
+		#: bool
 		self.collective = collective
+		#: bool
 		self.no_user_modification = no_user_modification
+		#: Value of :class:`AttributeTypeUsage`
 		self.usage = usage
+		#:
 		self.extensions = extensions or {}
 
 	def __str__(self):
+		'''Return attribute type definition string according to RFC4512
+
+		Example:
+
+			( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC4519: common name(s) for which the entity is known by' SUP name )
+
+		The string can be decoded into an equalivalent
+		:class:`AttributeTypeDefinition` object with :any:`from_str`.'''
 		tokens = ['(', self.oid]
 		if self.name:
 			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
@@ -390,6 +462,12 @@ class AttributeTypeDefinition:
 
 	@classmethod
 	def from_str(cls, string):
+		'''Decode attribute type definition string according to RFC4512
+
+		:returns: Equivalent attribute type definition object
+		:rtype: AttributeTypeDefinition
+
+		See :any:`__str__` for the string format.'''
 		tokens = tokenize(string)
 		parse_token(tokens, '(')
 		oid = parse_numericoid(tokens)
@@ -443,12 +521,16 @@ class AttributeTypeDefinition:
 
 	@property
 	def first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch matching rule'''
 		return self.oid
 
 class ObjectClassKind(enum.Enum):
-	'''Values for kind argument of `ObjectClass`'''
+	'''Values for :any:`ObjectClassDefinition.kind`'''
+	#:
 	ABSTRACT = enum.auto()
+	#:
 	STRUCTURAL = enum.auto()
+	#:
 	AUXILIARY = enum.auto()
 
 class ObjectClassDefinition:
@@ -456,17 +538,37 @@ class ObjectClassDefinition:
 	def __init__(self, oid, name=None, desc='', obsolete=False, sup=None,
 	             kind=ObjectClassKind.STRUCTURAL, must=None, may=None,
 	             extensions=None):
+		#: Numeric OID (string)
 		self.oid = oid
+		#: Short descriptive names (list of strings, may be empty)
 		self.name = name or []
+		#: Description (string, empty if there is none)
 		self.desc = desc
+		#: bool
 		self.obsolete = obsolete
+		#: OIDs and short descriptive names of superior object classes (list of
+		#: strings, may be empty)
 		self.sup = sup or []
+		#: Value of :class:`ObjectClassKind`
 		self.kind = kind
+		#: OIDs and short descriptive names of attribute types entries with the
+		#: object class must have (list of strings, may be empty)
 		self.must = must or []
+		#: OIDs and short descriptive names of attribute types entries with the
+		#: object class may have (list of strings, may be empty)
 		self.may = may or []
+		#:
 		self.extensions = extensions or {}
 
 	def __str__(self):
+		'''Return object class definition string according to RFC4512
+
+		Example:
+
+			( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )
+
+		The string can be decoded into an equalivalent
+		:class:`ObjectClassDefinition` object with :any:`from_str`.'''
 		tokens = ['(', self.oid]
 		if self.name:
 			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
@@ -486,6 +588,12 @@ class ObjectClassDefinition:
 
 	@classmethod
 	def from_str(cls, string):
+		'''Decode object class definition string according to RFC4512
+
+		:returns: Equivalent object class definition object
+		:rtype: ObjectClassDefinition
+
+		See :any:`__str__` for the string format.'''
 		tokens = tokenize(string)
 		parse_token(tokens, '(')
 		oid = parse_numericoid(tokens)
@@ -521,4 +629,5 @@ class ObjectClassDefinition:
 
 	@property
 	def first_component_oid(self):
+		'''Used by objectIdentifierFirstComponentMatch matching rule'''
 		return self.oid
diff --git a/ldapserver/schema/types.py b/ldapserver/schema/types.py
index c5f24df..8eeac16 100644
--- a/ldapserver/schema/types.py
+++ b/ldapserver/schema/types.py
@@ -18,40 +18,65 @@ __all__ = [
 class Syntax:
 	'''LDAP syntax for attribute and assertion values'''
 	def __init__(self, schema, definition, syntaxes_by_tag):
+		#:
 		self.schema = schema
+		#: Corresponding :class:`SyntaxDefinition` object
 		self.definition = definition
+		#: Numeric OID of syntax (string, e.g. ``'1.3.6.1.4.1.1466.115.121.1.15'``)
 		self.oid = definition.oid
+		#: Preferred name for syntax (same as :any:`oid`)
 		self.ref = self.oid
 		schema._register(self, self.oid, self.ref)
 		schema.syntaxes._register(self, self.oid, self.ref)
 		for tag in definition.compatability_tags:
 			syntaxes_by_tag.setdefault(tag, set()).add(self)
 		# Populated by MatchingRule.__init__
+		#: Set of all matching rules wihin the schema that can be applied to values of the syntax
 		self.compatible_matching_rules = set()
 
 	def __repr__(self):
-		return f'<ldapserver.schema.Syntax {self.oid}>'
+		return f'<ldapserver.schema.Syntax {self.ref}>'
 
 	def encode(self, value):
+		'''Encode value to its LDAP-specific encoding
+
+		:param value: Native value
+		:type value: any
+		:rtype: bytes'''
 		return self.definition.encode(self.schema, value)
 
 	def decode(self, raw_value):
+		'''Decode LDAP-specific encoding of a value
+
+		:param raw_value: LDAP-specific encoding
+		:type raw_value: bytes
+		:rtype: any'''
 		return self.definition.decode(self.schema, raw_value)
 
 class MatchingRule:
 	def __init__(self, schema, definition, syntaxes_by_tag):
+		#:
 		self.schema = schema
+		#: Corresponding :class:`MatchingRuleDefinition` object
 		self.definition = definition
+		#: Numeric OID of matching rule (string, e.g. ``'2.5.13.2'``)
 		self.oid = definition.oid
+		#: Syntax of assertion values. For
+		#: :any:`schema.SubstrMatchingRule.match_substr` the syntax of the
+		#: attribute's equality matching rule is used instead.
 		self.syntax = schema.syntaxes[definition.syntax]
+		#: List of short descriptive names of the matching rule (list of strings, e.g. ``['caseIgnoreMatch']``)
 		self.names = self.definition.name
+		#: Preferred name of matching rule (first item of :any:`names` or :any:`oid`, e.g. ``'caseIgnoreMatch'``)
 		self.ref = self.names[0] if self.names else self.oid
 		schema._register(self, self.oid, self.ref, *self.names)
 		schema.matching_rules._register(self, self.oid, self.ref, *self.names)
+		#: Set of all compatible syntaxes within the schema whose values the matching rule can be applied to
 		self.compatible_syntaxes = syntaxes_by_tag.setdefault(definition.compatability_tag, set())
 		for syntax in self.compatible_syntaxes:
 			syntax.compatible_matching_rules.add(self)
 		# Populated by AttributeType.__init__
+		#: Set of all compatible attribute types within the schema whose values the matching rule can be applied to
 		self.compatible_attribute_types = set()
 
 	def match_extensible(self, attribute_values, assertion_value):
@@ -62,12 +87,27 @@ class EqualityMatchingRule(MatchingRule):
 		return f'<ldapserver.schema.EqualityMatchingRule {self.ref}>'
 
 	def match_extensible(self, attribute_values, assertion_value):
+		'''Return whether any attribute value equals the assertion value
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		return self.definition.match_equal(self.schema, attribute_values, assertion_value)
 
 	def match_equal(self, attribute_values, assertion_value):
+		'''Return whether any attribute value equals the assertion value
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		return self.definition.match_equal(self.schema, attribute_values, assertion_value)
 
 	def match_approx(self, attribute_values, assertion_value):
+		'''Return whether any attribute value approximatly equals the assertion value
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		return self.definition.match_approx(self.schema, attribute_values, assertion_value)
 
 class OrderingMatchingRule(MatchingRule):
@@ -75,9 +115,19 @@ class OrderingMatchingRule(MatchingRule):
 		return f'<ldapserver.schema.OrderingMatchingRule {self.ref}>'
 
 	def match_less(self, attribute_values, assertion_value):
+		'''Return whether any attribute value is less than assertion value
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		return self.definition.match_less(self.schema, attribute_values, assertion_value)
 
 	def match_greater_or_equal(self, attribute_values, assertion_value):
+		'''Return whether any attribute value is greater than or equal to assertion value
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		return self.definition.match_greater_or_equal(self.schema, attribute_values, assertion_value)
 
 class SubstrMatchingRule(MatchingRule):
@@ -85,37 +135,79 @@ class SubstrMatchingRule(MatchingRule):
 		return f'<ldapserver.schema.SubstrMatchingRule {self.ref}>'
 
 	def match_extensible(self, attribute_values, assertion_value):
+		'''Return whether any attribute value matches the substring assertion in assertion_value
+
+		:param attribute_values: Attribute values (type according to attribute's syntax)
+		:type attribute_values: List of any
+		:param assertion_value: 3-tuple with initial substring, any substrings and final substring
+		:type assertion_value: Tuple[any or None, List[any] or None, any or None]
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		return self.definition.match_substr(self.schema, attribute_values, assertion_value[0], assertion_value[1], assertion_value[2])
 
 	def match_substr(self, attribute_values, inital_substring, any_substrings, final_substring):
+		'''Return whether any attribute value matches the substring assertion
+
+		The type of `inital_substring`, `any_substrings` and `final_substring`
+		depends on the syntax of the attribute's equality matching rule!
+
+		:param attribute_values: Attribute values (type according to attribute's syntax)
+		:type attribute_values: List of any
+		:param inital_substring: Substring to match the beginning (optional)
+		:type inital_substring: any
+		:param any_substrings: List of substrings to match between initial and final in order
+		:type any_substrings: list of any
+		:param final_substring: Substring to match the end (optional)
+		:type final_substring: any
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		return self.definition.match_substr(self.schema, attribute_values, inital_substring, any_substrings, final_substring)
 
 class AttributeType:
 	def __init__(self, schema, definition):
+		#:
 		self.schema = schema
+		#: Corresponding :class:`AttributeTypeDefinition` object
 		self.definition = definition
+		#: Numeric OID of the attribute type (string, e.g. ``'2.5.4.3'``)
 		self.oid = definition.oid
+		#: List of short descriptive names of the attribute type (list of strings, e.g. ``['cn', 'commonName']``)
 		self.names = definition.name or []
+		#: Preferred name of attribute type (first item of :any:`names` or :any:`oid`, e.g. ``'cn'``)
 		self.ref = self.names[0] if self.names else self.oid
+		#: Superior :class:`AttributeType` or `None`
 		self.sup = schema.attribute_types[definition.sup] if definition.sup else None
+		#: Set of subordinate attribute types
 		self.subtypes = set()
 		sup = self.sup
 		while sup:
 			self.sup.subtypes.add(self)
 			sup = sup.sup
+		#: :class:`EqualityMatchingRule` of the attribute type or `None` if the
+		#: attribute type has no equality matching rule
 		self.equality = schema.matching_rules[definition.equality] if definition.equality else None
+		#: :class:`OrderingMatchingRule` of the attribute type or `None` if the
+		#: attribute type has no ordering matching rule
 		self.ordering = schema.matching_rules[definition.ordering] if definition.ordering else None
+		#: :class:`SubstrMatchingRule` of the attribute type or `None` if the
+		#: attribute type has no substring matching rule
 		self.substr = schema.matching_rules[definition.substr] if definition.substr else None
 		if self.sup:
 			self.equality = self.equality or self.sup.equality
 			self.ordering = self.ordering or self.sup.ordering
 			self.substr = self.substr or self.sup.substr
+		#: :class:`Syntax` of attribute values
 		self.syntax = schema.syntaxes[definition.syntax] if definition.syntax else self.sup.syntax
+		#: :any:`True` if attribute type is operational, :any:`False` if it is a
+		#: user application attribute type.
 		self.is_operational = (definition.usage != AttributeTypeUsage.userApplications)
 		schema._register(self, self.oid, self.ref, *self.names)
 		schema.attribute_types._register(self, self.oid, self.ref, *self.names)
 		if not self.is_operational:
 			schema.user_attribute_types.add(self)
+		#: Set of all matching rules in schema that can be applied to values of the attribute type
 		self.compatible_matching_rules = self.syntax.compatible_matching_rules
 		for matching_rule in self.compatible_matching_rules:
 			matching_rule.compatible_attribute_types.add(self)
@@ -124,13 +216,23 @@ class AttributeType:
 		return f'<ldapserver.schema.AttributeType {self.ref}>'
 
 	def encode(self, value):
+		'''Encode attribute value to its LDAP-specific encoding
+
+		:param value: Native value
+		:type value: any
+		:rtype: bytes'''
 		return self.syntax.encode(value)
 
 	def decode(self, raw_value):
+		'''Decode LDAP-specific encoding of an attribute value
+
+		:param raw_value: LDAP-specific encoding
+		:type raw_value: bytes
+		:rtype: any'''
 		return self.syntax.decode(raw_value)
 
 	def match_equal(self, attribute_values, assertion_value):
-		'''Return whether any attribute value equals assertion value
+		'''Return whether any attribute value equals the assertion value
 
 		:param attribute_values: Attribute values (type according to syntax)
 		:type attribute_values: List of any
@@ -145,7 +247,7 @@ class AttributeType:
 		return self.equality.match_equal(attribute_values, assertion_value)
 
 	def match_substr(self, attribute_values, inital_substring, any_substrings, final_substring):
-		'''Return whether any attribute value matches a substring assertion
+		'''Return whether any attribute value matches the substring assertion
 
 		:param attribute_values: Attribute values (type according to syntax)
 		:type attribute_values: List of any
@@ -168,7 +270,7 @@ class AttributeType:
 		return self.substr.match_substr(attribute_values, inital_substring, any_substrings, final_substring)
 
 	def match_approx(self, attribute_values, assertion_value):
-		'''Return whether any attribute value approximatly equals assertion value
+		'''Return whether any attribute value approximatly equals the assertion value
 
 		:param attribute_values: Attribute values (type according to syntax)
 		:type attribute_values: List of any
@@ -183,6 +285,16 @@ class AttributeType:
 		return self.equality.match_approx(attribute_values, assertion_value)
 
 	def match_greater_or_equal(self, attribute_values, assertion_value):
+		'''Return whether any attribute value is greater than or equal to assertion value
+
+		:param attribute_values: Attribute values (type according to syntax)
+		:type attribute_values: List of any
+		:param assertion_value: Assertion value
+		:type assertion_value: bytes
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+
 		if self.ordering is None:
 			raise exceptions.LDAPInappropriateMatching()
 		assertion_value = self.ordering.syntax.decode(assertion_value)
@@ -195,6 +307,16 @@ class AttributeType:
 		return self.ordering.match_less(attribute_values, assertion_value)
 
 	def match_less_or_equal(self, attribute_values, assertion_value):
+		'''Return whether any attribute value is less than or equal to assertion value
+
+		:param attribute_values: Attribute values (type according to syntax)
+		:type attribute_values: List of any
+		:param assertion_value: Assertion value
+		:type assertion_value: bytes
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+
 		equal_exc = None
 		try:
 			if self.match_equal(attribute_values, assertion_value):
@@ -208,6 +330,17 @@ class AttributeType:
 		return False
 
 	def match_extensible(self, attribute_values, assertion_value, matching_rule=None):
+		'''Return whether any attribute value matches the assertion value
+
+		:param attribute_values: Attribute values (type according to syntax)
+		:type attribute_values: List of any
+		:param assertion_value: Assertion value
+		:type assertion_value: bytes
+		:param matching_rule: Optional matching rule, if None :any:`AttributeType.equality` is used
+		:type matching_rule: :class:`EqualityMatchingRule`, :class:`OrderingMatchingRule`, :class:`SubstrMatchingRule` or None
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
 		if not matching_rule:
 			matching_rule = self.equality
 		if not matching_rule or matching_rule not in self.compatible_matching_rules:
@@ -218,10 +351,15 @@ class AttributeType:
 class ObjectClass:
 	'''Representation of an object class wihin a schema'''
 	def __init__(self, schema, definition):
+		#:
 		self.schema = schema
+		#: Corresponding :class:`AttributeTypeDefinition` object
 		self.definition = definition
+		#: Numeric OID of object class (string, e.g. ``'2.5.6.0'``)
 		self.oid = definition.oid
+		#: List of short descriptive names of the object class (list of strings, e.g. ``['top']``)
 		self.names = definition.name
+		#: Preferred name of object class (first item of :any:`names` or :any:`oid`, e.g. ``'top'``)
 		self.ref = self.names[0] if self.names else self.oid
 		# Lookup dependencies to ensure consistency
 		# pylint: disable=pointless-statement
@@ -275,8 +413,17 @@ class OIDDict(collections.abc.Mapping):
 		return self.__numeric_oid_map.get(key, default)
 
 class Schema(OIDDict):
-	'''Collection of LDAP syntaxes, matching rules, attribute types and object
-	classes forming an LDAP schema.'''
+	'''Consistent collection of syntaxes, matching rules, attribute types and
+	object classes forming an LDAP schema
+
+	:param object_class_definitions: List of :class:`ObjectClassDefinition` or :any:`str`
+	:param attribute_type_definitions: List of :class:`AttributeTypeDefinition` or :any:`str`
+	:param matching_rule_definitions: List of :class:`MatchingRuleDefinition`
+	:param syntax_definitions: List of :class:`SyntaxDefinition`
+
+	Is also a mapping of OIDs and short descriptive names to the respective
+	schema element objects (:any:`Syntax`, ...).
+	'''
 	# pylint: disable=too-many-branches,too-many-statements
 	def __init__(self, object_class_definitions=None, attribute_type_definitions=None,
 	             matching_rule_definitions=None, syntax_definitions=None):
@@ -284,13 +431,16 @@ class Schema(OIDDict):
 		syntaxes_by_tag = {}
 
 		# Add syntaxes
+		#: Mapping of syntax OIDs to :any:`Syntax` objects
 		self.syntaxes = OIDDict()
 		for definition in syntax_definitions or []:
 			if definition.oid not in self.syntaxes:
 				Syntax(self, definition, syntaxes_by_tag)
+		#: Sequence of :any:`SyntaxDefinition` objects
 		self.syntax_definitions = [syntax.definition for syntax in self.syntaxes.values()]
 
 		# Add matching rules
+		#: Mapping of matching rule OIDs and short descriptive names to :any:`EqualityMatchingRule`, :any:`OrderingMatchingRule` and :any:`SubstrMatchingRule`  objects
 		self.matching_rules = OIDDict()
 		for definition in matching_rule_definitions or []:
 			if definition.kind == MatchingRuleKind.EQUALITY:
@@ -303,12 +453,14 @@ class Schema(OIDDict):
 				raise ValueError('Invalid matching rule kind')
 			if definition.oid not in self.matching_rules:
 				cls(self, definition, syntaxes_by_tag)
+		#: Sequence of :any:`MatchingRuleDefinition` objects
 		self.matching_rule_definitions = [matching_rule.definition for matching_rule in self.matching_rules.values()]
 
 		# Add attribute types
 		attribute_type_definitions = [AttributeTypeDefinition.from_str(item)
 		                              if isinstance(item, str) else item
 		                              for item in attribute_type_definitions or []]
+		#: Mapping of attribute type OIDs and short descriptive names to :any:`AttributeType` objects
 		self.attribute_types = OIDDict()
 		self.user_attribute_types = set()
 		# Attribute types may refer to other (superior) attribute types. To resolve
@@ -328,12 +480,14 @@ class Schema(OIDDict):
 		for definition in attribute_type_definitions:
 			if definition.oid not in self.attribute_types:
 				AttributeType(self, definition)
+		#: Sequence of :any:`AttributeTypeDefinition` objects
 		self.attribute_type_definitions = [attribute_type.definition for attribute_type in self.attribute_types.values()]
 
 		# Add object classes
 		object_class_definitions = [ObjectClassDefinition.from_str(item)
 		                            if isinstance(item, str) else item
 		                            for item in object_class_definitions or []]
+		#: Mapping of object class OIDs and short descriptive names to :any:`ObjectClass` objects
 		self.object_classes = OIDDict()
 		# Object classes may refer to other (superior) object classes. To resolve
 		# these dependencies we cycle through the definitions, each time adding
@@ -352,9 +506,11 @@ class Schema(OIDDict):
 		for definition in object_class_definitions:
 			if definition.oid not in self.object_classes:
 				ObjectClass(self, definition)
+		#: Sequence of :any:`ObjectClassDefinition` objects
 		self.object_class_definitions = [object_class.definition for object_class in self.object_classes.values()]
 
 		# Generate and add matching rules
+		#: Sequence of :any:`MatchingRuleUseDefinition` objects
 		self.matching_rule_use_definitions = []
 		for matching_rule in self.matching_rules.values():
 			definition = matching_rule.definition
@@ -367,6 +523,7 @@ class Schema(OIDDict):
 
 	def extend(self, object_class_definitions=None, attribute_type_definitions=None,
 	           matching_rule_definitions=None, syntax_definitions=None):
+		'''Return new schema with all schema elements and additional ones'''
 		syntax_definitions = self.syntax_definitions + (syntax_definitions or [])
 		matching_rule_definitions = self.matching_rule_definitions + (matching_rule_definitions or [])
 		attribute_type_definitions = self.attribute_type_definitions + (attribute_type_definitions or [])
diff --git a/ldapserver/server.py b/ldapserver/server.py
index 3a7d5d8..1c58592 100644
--- a/ldapserver/server.py
+++ b/ldapserver/server.py
@@ -44,6 +44,11 @@ class RequestLogAdapter(logging.LoggerAdapter):
 		return self.extra['trace_id'] + ': ' + msg, kwargs
 
 class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
+	#: Logger for request processing
+	#:
+	#: For every connection the logger object is wrapped with a
+	#: :any:`logging.LoggerAdapter` that prefixes messages with a unique token
+	#: for connetcion tracing.
 	logger = logging.getLogger('ldapserver.server')
 
 	def setup(self):
@@ -73,11 +78,6 @@ class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
 		self.logger.info('Disconnected duration_seconds=%.3f', time_disconnect - time_connect)
 
 	def handle_message(self, shallowmsg: ldap.ShallowLDAPMessage) -> typing.Iterable[ldap.LDAPMessage]:
-		'''Handle an LDAP request foobar
-
-		:param shallowmsg: Half-decoded LDAP message to handle
-		:returns: Response messages
-		'''
 		msgtypes = {
 			ldap.BindRequest: (self.handle_bind, ldap.BindResponse),
 			ldap.UnbindRequest: (self.handle_unbind, None),
@@ -166,26 +166,19 @@ class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
 		raise exceptions.LDAPProtocolError()
 
 class LDAPRequestHandler(BaseLDAPRequestHandler):
-	'''
-	.. py:attribute:: rootdse
-
-		Special :any:`LDAPObject` that contains information
-		about the server, such as supported extentions and SASL authentication
-		mechansims. Attributes can be accessed in a dict-like fashion.
-	'''
-
+	#: :class:`SubschemaSubentry` object that describes the schema. Default
+	#: value uses :any:`schema.RFC4519_SCHEMA`. Returned by :any:`do_search`.
 	subschema = objects.SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema', cn=['Subschema'])
-	'''
-	.. py:attribute:: subschema
 
-		Special :any:`LDAPObject` that describes the schema.
-		Per default the subschema includes standard syntaxes, standard matching
-		rules and objectclasses/attributetypes for the rootdse and subschema.
-		It does not include objectclasses/attributetypes for actual data
-		(e.g. users and groups). See :any:`Subschema` for details.
-	'''
+	#: :class:`RootDSE` object containing information about the server, such
+	#: as supported extentions and SASL authentication mechansims. Content is
+	#: determined on setup based on the `supports_*` attributes. Returned by
+	#: :any:`do_search`.
+	rootdse: typing.Any
 
-	static_objects = tuple()
+	#: Opaque bind/authorization state. Initially `None` and set to `None` on
+	#: anonymous bind. Set to whatever the `do_bind_*` callbacks return.
+	bind_object: typing.Any
 
 	def setup(self):
 		super().setup()
@@ -207,9 +200,9 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 			self.rootdse['supportedSASLMechanisms'].append('EXTERNAL')
 		self.rootdse['supportedLDAPVersion'] = ['3']
 		self.bind_object = None
-		self.bind_sasl_state = None
+		self.__bind_sasl_state = None # Set to (mechanism, iterator) by handle_bind
 		self.__paged_searches = {} # pagination cookie -> (iterator, orig_op)
-		self.__paged_cookie_counter = 0
+		self.__paged_cookie_counter = 0 # Used to generate unique cookie values
 
 	def handle_bind(self, op, controls=None):
 		reject_critical_controls(controls)
@@ -217,21 +210,21 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 			raise exceptions.LDAPProtocolError('Unsupported protocol version')
 		auth = op.authentication
 		# Resume ongoing SASL dialog
-		if self.bind_sasl_state and isinstance(auth, ldap.SaslCredentials) \
-				and auth.mechanism == self.bind_sasl_state[0]:
-			mechanism, iterator = self.bind_sasl_state
-			self.bind_sasl_state = None
+		if self.__bind_sasl_state and isinstance(auth, ldap.SaslCredentials) \
+				and auth.mechanism == self.__bind_sasl_state[0]:
+			mechanism, iterator = self.__bind_sasl_state
+			self.__bind_sasl_state = None
 			resp_code = ldap.LDAPResultCode.saslBindInProgress
 			try:
 				resp = iterator.send(auth.credentials)
-				self.bind_sasl_state = (mechanism, iterator)
+				self.__bind_sasl_state = (mechanism, iterator)
 			except StopIteration as e:
 				resp_code = ldap.LDAPResultCode.success
 				self.bind_object, resp = e.value # pylint: disable=unpacking-non-sequence
 			yield ldap.BindResponse(resp_code, serverSaslCreds=resp)
 			return
 		# If auth type or SASL method changed, abort SASL dialog
-		self.bind_sasl_state = None
+		self.__bind_sasl_state = None
 		if isinstance(auth, ldap.SimpleAuthentication):
 			self.bind_object = self.do_bind_simple(op.name, auth.password)
 			yield ldap.BindResponse(ldap.LDAPResultCode.success)
@@ -245,7 +238,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 			resp_code = ldap.LDAPResultCode.saslBindInProgress
 			try:
 				resp = next(iterator)
-				self.bind_sasl_state = (auth.mechanism, iterator)
+				self.__bind_sasl_state = (auth.mechanism, iterator)
 			except StopIteration as e:
 				resp_code = ldap.LDAPResultCode.success
 				self.bind_object, resp = e.value # pylint: disable=unpacking-non-sequence
@@ -256,7 +249,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 	def do_bind_simple(self, dn='', password=b''):
 		'''Do LDAP BIND with simple authentication
 
-		:param dn: Distinguished name of object to be authenticated or empty
+		:param dn: Distinguished name of object to be authenticated, may be empty
 		:type dn: str
 		:param password: Password, may be empty
 		:type password: bytes
@@ -276,27 +269,21 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 	def do_bind_simple_anonymous(self):
 		'''Do LDAP BIND with simple anonymous authentication (`RFC 4513 5.1.1.`_)
 
-		:raises exceptions.LDAPError: if authentication failed
+		:raises exceptions.LDAPInvalidCredentials: if authentication failed
+		:returns: Bind object on success (see :any:`bind_object`)
 
-		:returns: Bind object on success
-		:rtype: obj
-
-		Calld by :any:`do_bind_simple`. Always returns None.'''
+		The default implementation always returns None.'''
 		return None
 
 	def do_bind_simple_unauthenticated(self, dn):
 		'''Do LDAP BIND with simple unauthenticated authentication (`RFC 4513 5.1.2.`_)
 
-		:param dn: Distinguished name of the object to be authenticated
+		:param dn: DN of the object to be authenticated as
 		:type dn: str
+		:raises exceptions.LDAPInvalidCredentials: if authentication failed
+		:returns: Bind object on success (see :any:`bind_object`)
 
-		:raises exceptions.LDAPError: if authentication failed
-
-		:returns: Bind object on success
-		:rtype: obj
-
-		Calld by :any:`do_bind_simple`. The default implementation always raises an
-		:any:`LDAPInvalidCredentials` exception.'''
+		The default implementation always raises :any:`LDAPInvalidCredentials`.'''
 		raise exceptions.LDAPInvalidCredentials()
 
 	def do_bind_simple_authenticated(self, dn, password):
@@ -305,15 +292,11 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		:param dn: Distinguished name of the object to be authenticated
 		:type dn: str
 		:param password: Password for object
-		:type dn: bytes
-
-		:raises exceptions.LDAPError: if authentication failed
-
-		:returns: Bind object on success
-		:rtype: obj
+		:type password: bytes
+		:raises exceptions.LDAPInvalidCredentials: if authentication failed
+		:returns: Bind object on success (see :any:`bind_object`)
 
-		Calld by :any:`do_bind_simple`. The default implementation always raises an
-		`LDAPInvalidCredentials` exception.'''
+		The default implementation always raises :any:`LDAPInvalidCredentials`.'''
 		raise exceptions.LDAPInvalidCredentials()
 
 	def do_bind_sasl(self, mechanism, credentials=None, dn=None):
@@ -363,6 +346,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 			return self.do_bind_sasl_external(authzid=credentials), None
 		raise exceptions.LDAPAuthMethodNotSupported()
 
+	#: Indicate SASL "ANONYMOUS" support
 	supports_sasl_anonymous = False
 
 	def do_bind_sasl_anonymous(self, trace_info=None):
@@ -377,10 +361,11 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		:returns: Bind object on success
 		:rtype: obj
 
-		Calld by :any:`do_bind_sasl`. The default implementation raises an
-		:any:`LDAPAuthMethodNotSupported` exception.'''
+		Only called if :any:`supports_sasl_anonymous` is True.
+		The default implementation raises :any:`LDAPAuthMethodNotSupported`.'''
 		raise exceptions.LDAPAuthMethodNotSupported()
 
+	#: Indicate SASL "PLAIN" support
 	supports_sasl_plain = False
 
 	def do_bind_sasl_plain(self, identity, password, authzid=None):
@@ -398,10 +383,11 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		:returns: Bind object on success
 		:rtype: obj
 
-		Calld by :any:`do_bind_sasl`. The default implementation raises an
-		:any:`LDAPAuthMethodNotSupported` exception.'''
+		Only called if :any:`supports_sasl_plain` is True.
+		The default implementation raises :any:`LDAPAuthMethodNotSupported`.'''
 		raise exceptions.LDAPAuthMethodNotSupported()
 
+	#: Indicate SASL "EXTERNAL" support
 	supports_sasl_external = False
 
 	def do_bind_sasl_external(self, authzid=None):
@@ -418,10 +404,13 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		EXTERNAL is commonly used for TLS client certificate authentication or
 		system user based authentication on UNIX sockets.
 
-		Calld by :any:`do_bind_sasl`. The default implementation raises an
-		:any:`LDAPAuthMethodNotSupported` exception.'''
+		Only called if :any:`supports_sasl_external` is True.
+		The default implementation raises :any:`LDAPAuthMethodNotSupported`.'''
 		raise exceptions.LDAPAuthMethodNotSupported()
 
+	#: Enable/disable support for "Simple Paged Results Manipulation" control
+	#: (RFC2696). Paginated search uses :any:`do_search` like non-paginated
+	#: search does.
 	supports_paged_results = True
 
 	def __handle_search_paged(self, op, paged_control, controls=None):
@@ -488,25 +477,31 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		                 op.baseObject, op.scope.name, op.filter, ' '.join(op.attributes), entries, time_end - time_start)
 
 	def do_search(self, baseobj, scope, filterobj):
-		'''Do LDAP SEARCH operation
+		'''Return result candidates for a SEARCH operation
 
 		:param baseobj: Distinguished name of the LDAP entry relative to which the
 		                search is to be performed
 		:type baseobj: str
 		:param scope: Search scope
-		:type scope: SearchScope
+		:type scope: ldap.SearchScope
 		:param filterobj: Filter object
-		:type filterobj: Filter
-
+		:type filterobj: ldap.Filter
 		:raises exceptions.LDAPError: on error
+		:returns: All LDAP objects that might match the parameters of the SEARCH
+		          operation.
+		:rtype: Iterable of :class:`Object`
 
-		:returns: Iterable of dn, attributes tuples
+		The default implementation yields :any:`rootdse` and :any:`subschema`.
+		Both are importent for feature detection, so make sure to also return
+		them (e.g. with ``yield from super().do_search(...)``).
 
-		The default implementation returns matching objects from the root dse and
-		the subschema.'''
+		For every returned object :any:`Object.search` is called to filter out
+		non-matching objects and to construct the response.
+
+		Note that if this method is as an iterator, its execution may be paused
+		for extended periods of time or aborted prematurly.'''
 		yield self.rootdse
 		yield self.subschema
-		yield from self.static_objects
 
 	def handle_compare(self, op, controls=None):
 		self.logger.info('COMPRAE request "%s" %s=%s', op.entry, op.ava.attributeDesc, repr(op.ava.assertionValue))
@@ -529,13 +524,12 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		:type attribute: str
 		:param value: Attribute value
 		:type value: bytes
-
 		:raises exceptions.LDAPError: on error
-
 		:returns: `Object` or None
 
 		The default implementation calls `do_search` and returns the first object
-		with the right DN.'''
+		for which :any:`Object.compare` does not raise
+		:any:`exceptions.LDAPNoSuchObject` (i.e. the object has the requested DN).'''
 		objs = self.do_search(dn, ldap.SearchScope.baseObject, ldap.FilterPresent(attribute='objectClass'))
 		for obj in objs:
 			try:
-- 
GitLab