Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better property support #640

Open
wants to merge 64 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
bd7ca7b
Initial attempt at fixing #587: Unify properties getter, setters and …
tristanlatr Aug 5, 2022
5758a37
More property demo
tristanlatr Aug 5, 2022
eb8f49e
Fix test
tristanlatr Aug 5, 2022
51935e6
Better understanding properties
tristanlatr Aug 11, 2022
11ac561
fix test
tristanlatr Aug 11, 2022
4c812ac
More tests
tristanlatr Aug 12, 2022
6935be1
Don't use list() to create a list of attribute to initiate.
tristanlatr Aug 12, 2022
32554af
Better checks before adding new inherited elements to property
tristanlatr Aug 12, 2022
b3c6d01
Add overriden properties to the RST demo.
tristanlatr Aug 16, 2022
643069a
Merge branch 'master' into 587-better-property-support
tristanlatr Sep 8, 2022
4fb19c2
fix import
tristanlatr Nov 18, 2022
a3f2519
add assert
tristanlatr Nov 18, 2022
96fd437
Merge branch 'master' into 587-better-property-support
tristanlatr Nov 18, 2022
bdd449f
fix mypy
tristanlatr Nov 18, 2022
67ec358
simplify the code a little bit
tristanlatr Nov 18, 2022
dbf0477
simplify again
tristanlatr Nov 18, 2022
e7c73a5
Simplify again, no need to tell the users the property is read-write …
tristanlatr Nov 18, 2022
d084224
Simplify more
tristanlatr Nov 18, 2022
111b669
remove commented code
tristanlatr Nov 19, 2022
991d0a4
Try to simplify more
tristanlatr Nov 19, 2022
b6ab83d
Refactors
tristanlatr Nov 19, 2022
6cbb8a8
Merge branch 'master' into 587-better-property-support
tristanlatr Feb 1, 2023
d058d43
Merge branch 'master' into 587-better-property-support
tristanlatr Apr 6, 2023
d3d5d51
Simplify
tristanlatr Apr 6, 2023
346b40d
Use impossible import cycle in test
tristanlatr Apr 6, 2023
063ded2
Minor refactor
tristanlatr Apr 6, 2023
9dccd4f
Minor refactors
tristanlatr Apr 6, 2023
49eca75
Merge branch 'master' into 587-better-property-support
tristanlatr Apr 6, 2023
a1529ee
Fix import
tristanlatr Apr 6, 2023
6203a04
Fix mypy
tristanlatr Apr 6, 2023
272e7db
Fix crash :/
tristanlatr Apr 6, 2023
88cb433
Fix pyflakes
tristanlatr Apr 6, 2023
5acd371
Create a legit model object for Properties.
tristanlatr Apr 7, 2023
ceacac0
Refoctors to have a more simple model.
tristanlatr Apr 7, 2023
91c34bf
Minor comment
tristanlatr Apr 7, 2023
a45ebe2
Add Property model to the extension system.
tristanlatr Apr 23, 2023
ff6a662
Simplify implementation
tristanlatr Apr 23, 2023
2532a06
Simplify propertyInfo renderer
tristanlatr Apr 23, 2023
f2632a7
add missing PropertyMixin to mixinT
tristanlatr Apr 23, 2023
4383240
Refacfors
tristanlatr Apr 24, 2023
21a047a
Update docs/epytext_demo/demo_epytext_module.py
tristanlatr Jun 4, 2023
4f6b591
Update docs/restructuredtext_demo/demo_restructuredtext_module.py
tristanlatr Jun 4, 2023
91afd5b
Merge branch '587-better-property-support' of github.com:twisted/pydo…
tristanlatr Jun 4, 2023
3eed08a
Merge branch 'master' into 587-better-property-support
tristanlatr Jun 4, 2023
de67845
Remove inherited property support. Add support for old school property()
tristanlatr Jun 5, 2023
4b8a0ff
Avoid calling 'signature(property)', it fails on some python versions.
tristanlatr Jun 5, 2023
c635451
Trigger warnings when a docstring is beeing overriden.
tristanlatr Jun 5, 2023
198f91d
Fix mypy and few refactors
tristanlatr Jun 5, 2023
d2e143b
Typo
tristanlatr Jun 5, 2023
4e174e2
Merge branch 'master' into 587-better-property-support
tristanlatr Sep 10, 2023
917947a
Merge branch 'master' into 587-better-property-support
tristanlatr Sep 12, 2023
ef4c7c0
Merge branch 'master' into 587-better-property-support
tristanlatr Sep 28, 2023
e6b9bef
Merge branch 'master' into 587-better-property-support
tristanlatr Sep 28, 2023
bd73b7e
Merge branch 'master' into 587-better-property-support
tristanlatr Nov 3, 2023
ac52177
Merge branch 'master' into 587-better-property-support
tristanlatr Jan 17, 2024
d7bdff5
Re-add support for inherited property support.
tristanlatr Jan 17, 2024
5f2f3ee
Merge branch 'master' into 587-better-property-support
tristanlatr Jan 18, 2024
af08b4b
Fix pyflakes
tristanlatr Jan 18, 2024
f088646
Merge branch '587-better-property-support' of github.com:twisted/pydo…
tristanlatr Jan 18, 2024
58c941b
Minor refactors
tristanlatr Jan 18, 2024
1bb7bed
Make sure we don't remove property functions when the property is cre…
tristanlatr Jan 21, 2024
238f386
Merge branch 'master' into 587-better-property-support
tristanlatr Jun 20, 2024
dec3483
Factor out some logic of the function def handler that is becoming to…
tristanlatr Jun 21, 2024
323e59d
Merge branch 'master' into 587-better-property-support
tristanlatr Jul 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/epytext_demo/demo_epytext_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,14 @@ def read_and_write(self) -> int:
@read_and_write.setter
def read_and_write(self, value: int) -> None:
"""
This is a docstring for setter.
This is a docstring for setter.
Their are usually not explicitely documented though.
"""

@property
def read_and_write_delete(self) -> int:
"""
This is a read-write-delete property.
This is the docstring of the property.
"""
return 1

Expand All @@ -183,6 +184,13 @@ def read_and_write_delete(self) -> None:
"""
This is a docstring for deleter.
"""

@property
def undoc_prop(self) -> bytes:
"""This property has a docstring only on the setter."""
tristanlatr marked this conversation as resolved.
Show resolved Hide resolved
@undoc_prop.setter
def undoc_prop(self, p) -> None: # type:ignore
...


class IContact(zope.interface.Interface):
Expand Down
36 changes: 34 additions & 2 deletions docs/restructuredtext_demo/demo_restructuredtext_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ def _private_inside_private(self) -> List[str]:
:rtype: `list`
"""
return []

@property
def isPrivate(self) -> bool:
"""Whether this class is private"""
return True
@isPrivate.setter
def isPrivate(self, v) -> bool:
raise NotImplemented()

@property
def isPublic(self) -> bool:
"""Whether this class is public"""
return False
@isPublic.setter
def isPublic(self, v) -> bool:
raise NotImplemented()



Expand Down Expand Up @@ -178,13 +194,14 @@ def read_and_write(self) -> int:
@read_and_write.setter
def read_and_write(self, value: int) -> None:
"""
This is a docstring for setter.
This is a docstring for setter.
Their are usually not explicitely documented though.
"""

@property
def read_and_write_delete(self) -> int:
"""
This is a read-write-delete property.
This is the docstring of the property.
"""
return 1

Expand All @@ -199,6 +216,21 @@ def read_and_write_delete(self) -> None:
"""
This is a docstring for deleter.
"""

@property
def undoc_prop(self) -> bytes:
"""This property has a docstring only on the setter."""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""This property has a docstring only on the setter."""
"""This property has a docstring only on the getter."""

tristanlatr marked this conversation as resolved.
Show resolved Hide resolved
@undoc_prop.setter
def undoc_prop(self, p) -> None: # type:ignore
...

@property
def isPrivate(self) -> bool:
return False

@_PrivateClass.isPublic.setter
def isPublic(self, v):
self._v = v

class IContact(zope.interface.Interface):
"""
Expand Down
2 changes: 1 addition & 1 deletion docs/tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def test_search(query:str, expected:List[str], order_is_important:bool=True) ->
['pydoctor.model.Class',
'pydoctor.factory.Factory.Class',
'pydoctor.model.DocumentableKind.CLASS',
'pydoctor.model.System.Class'])
'pydoctor.model.System.Class'], order_is_important=False)

to_stan_results = [
'pydoctor.epydoc.markup.ParsedDocstring.to_stan',
Expand Down
202 changes: 150 additions & 52 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
)

import astor
from pydoctor import epydoc2stan, model, node2stan, extensions
import attr
from pydoctor import epydoc2stan, model, node2stan, extensions, astutils
from pydoctor.epydoc.markup._pyval_repr import colorize_inline_pyval
from pydoctor.astutils import (is_none_literal, is_typing_annotation, is_using_annotations, is_using_typing_final, node2dottedname, node2fullname,
is__name__equals__main__, unstring_annotation, iterassign, extract_docstring_linenum,
Expand Down Expand Up @@ -147,6 +148,73 @@ def extract_final_subscript(annotation: ast.Subscript) -> ast.expr:
assert isinstance(ann_slice, ast.expr)
return ann_slice

def is_property_def_decorator(dottedname:List[str], ctx:model.Documentable) -> bool:
"""
Whether the last element of the list of names finishes by "property" or "Property".
"""
if len(dottedname) >= 1 and (dottedname[-1].endswith('property') or dottedname[-1].endswith('Property')):
# TODO: Support property subclasses.
return True
return False

def looks_like_property_func_decorator(dottedname:List[str], ctx:model.Documentable) -> bool:
"""
Whether the last element of the list of names is "getter", "setter" or "deleter".
Won't match C{['geter']}, because we require the list to have a least 2 elements.
"""
if len(dottedname) >= 2 and dottedname[-1] in ('getter' ,'setter', 'deleter'):
return True
return False

def get_inherited_property(dottedname:List[str], _parent: model.Documentable) -> Optional[model.PropertyDef]:
"""
Fetch the inherited property that this new decorator overrides.
None if it doesn't exist.
"""
if not get_property_function_kind(dottedname):
return None

_property_name = dottedname[:-1]

if len(_property_name) <= 1 or _property_name[-1] in _parent.contents:
# the property already exist
return None

# attr can be a getter/setter/deleter
_cls = _parent.resolveName('.'.join(_property_name[:-1]))
if _cls is None or not isinstance(_cls, model.Class):
# Can't make sens of property decorator
# the property decorator is pointing to something external OR
# not found in the system yet because of cyclic imports
# OR to something else than a class :/
# Don't rename it.
return None

# The class on which the property is defined (_cls) does not have
# to be in the MRO of the parent
attr_def = _cls.find(_property_name[-1])
if not isinstance(attr_def, model.Attribute):
return None

if not attr_def.kind is model.DocumentableKind.PROPERTY:
return None

return attr_def.property_def

def get_property_function_kind(dottedname:List[str]) -> Optional[model.PropertyFunctionKind]:
"""
What kind of property function this decorator declares?
None if we can't make sens of the decorator.
"""
if dottedname:
if dottedname[-1] == 'setter':
return model.PropertyFunctionKind.SETTER
if dottedname[-1] == 'getter':
return model.PropertyFunctionKind.GETTER
if dottedname[-1] == 'deleter':
return model.PropertyFunctionKind.DELETER
return None

class ModuleVistor(NodeVisitor):

def __init__(self, builder: 'ASTBuilder', module: model.Module):
Expand Down Expand Up @@ -766,41 +834,84 @@ def _handleFunctionDef(self,
func_name = node.name

# determine the function's kind
is_property = False

is_classmethod = False
is_staticmethod = False
is_overload_func = False

is_property = False
property_decorator_dottedname: Optional[List[str]] = None

if node.decorator_list:
for d in node.decorator_list:
if isinstance(d, ast.Call):
deco_name = node2dottedname(d.func)
else:
deco_name = node2dottedname(d)
for deco_name, _ in astutils.iter_decorator_list(node.decorator_list):
if deco_name is None:
continue
if isinstance(parent, model.Class):
if deco_name[-1].endswith('property') or deco_name[-1].endswith('Property'):
if is_property_def_decorator(deco_name, parent):
is_property = True
elif deco_name == ['classmethod']:
is_classmethod = True
elif deco_name == ['staticmethod']:
is_staticmethod = True
elif len(deco_name) >= 2 and deco_name[-1] in ('setter', 'deleter'):
# Pre-handle property elements
elif looks_like_property_func_decorator(deco_name, parent):
# Setters and deleters should have the same name as the property function,
# otherwise ignore it.
# This pollutes the namespace unnecessarily and is generally not recommended.
# Therefore it makes sense to stick to a single name,
# which is consistent with the former property definition.
if not deco_name[-2] == func_name:
continue

# Rename the setter/deleter, so it doesn't replace
# the property object.

func_name = '.'.join(deco_name[-2:])
property_decorator_dottedname = deco_name

# Determine if the function is decorated with overload
if parent.expandName('.'.join(deco_name)) in ('typing.overload', 'typing_extensions.overload'):
is_overload_func = True

if is_property:
# handle property and skip child nodes.
attr = self._handlePropertyDef(node, docstring, lineno)
if is_classmethod:
attr.report(f'{attr.fullName()} is both property and classmethod')
if is_staticmethod:
attr.report(f'{attr.fullName()} is both property and staticmethod')
raise self.SkipNode()

func_property: Optional[model.Attribute] = None
prop_func_kind: Optional[model.PropertyFunctionKind] = None
is_new_property: bool = is_property

if property_decorator_dottedname is not None:

prop_func_kind = get_property_function_kind(property_decorator_dottedname)

# Looks like inherited property
if len(property_decorator_dottedname)>2:
inherited_property = get_inherited_property(property_decorator_dottedname, parent)
if inherited_property:
func_property = self.builder.addAttribute(node.name,
kind=model.DocumentableKind.PROPERTY,
parent=parent)
func_property.setLineNumber(lineno)
func_property.decorators = node.decorator_list
# copy property info
func_property.property_def = inherited_property.clone()
is_new_property = True

else:
# fetch property info to add this info to it
maybe_prop = self.builder.current.contents.get(node.name)
if isinstance(maybe_prop, model.Attribute) and maybe_prop.property_def:
func_property = maybe_prop

elif is_property:
func_property = self.builder.addAttribute(node.name,
kind=model.DocumentableKind.PROPERTY,
parent=parent)
func_property.setLineNumber(lineno)
func_property.decorators = node.decorator_list
prop_func_kind = model.PropertyFunctionKind.GETTER
# rename func X.getter
func_name = node.name+'.getter'

# Push and analyse function

# Check if it's a new func or exists with an overload
existing_func = parent.contents.get(func_name)
Expand All @@ -815,7 +926,14 @@ def _handleFunctionDef(self,
# Do not recreate function object, just re-push it
self.builder.push(existing_func, lineno)
func = existing_func
elif isinstance(existing_func, model.Function) and func_property is not None and not is_new_property:
# Check if this property function is overriding a previously defined
# property function on the same scope before pushing the new function
# If it does override something, just re-push the function, do not override it.
self.builder.push(existing_func, lineno)
func = existing_func
else:
# create new function
func = self.builder.pushFunction(func_name, lineno)

func.is_async = is_async
Expand Down Expand Up @@ -887,48 +1005,25 @@ def add_arg(name: str, kind: Any, default: Optional[ast.expr]) -> None:
func.overloads.append(model.FunctionOverload(primary=func, signature=signature, decorators=node.decorator_list))
else:
func.signature = signature

if func_property is not None:

if is_classmethod:
func_property.report(f'{func_property.fullName()} is both property and classmethod')
if is_staticmethod:
func_property.report(f'{func_property.fullName()} is both property and staticmethod')

assert prop_func_kind is not None
# Store the fact that this function implements one of the getter/setter/deleter
func_property.property_def.set(prop_func_kind, func)


def depart_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
self.builder.popFunction()

def depart_FunctionDef(self, node: ast.FunctionDef) -> None:
self.builder.popFunction()

def _handlePropertyDef(self,
node: Union[ast.AsyncFunctionDef, ast.FunctionDef],
docstring: Optional[ast.Str],
lineno: int
) -> model.Attribute:

attr = self.builder.addAttribute(name=node.name, kind=model.DocumentableKind.PROPERTY, parent=self.builder.current)
attr.setLineNumber(lineno)

if docstring is not None:
attr.setDocstring(docstring)
assert attr.docstring is not None
pdoc = epydoc2stan.parse_docstring(attr, attr.docstring, attr)
other_fields = []
for field in pdoc.fields:
tag = field.tag()
if tag == 'return':
if not pdoc.has_body:
pdoc = field.body()
# Avoid format_summary() going back to the original
# empty-body docstring.
attr.docstring = ''
elif tag == 'rtype':
attr.parsed_type = field.body()
else:
other_fields.append(field)
pdoc.fields = other_fields
attr.parsed_docstring = pdoc

if node.returns is not None:
attr.annotation = unstring_annotation(node.returns, attr)
attr.decorators = node.decorator_list

return attr

def _annotations_from_function(
self, func: Union[ast.AsyncFunctionDef, ast.FunctionDef]
) -> Mapping[str, Optional[ast.expr]]:
Expand Down Expand Up @@ -1155,6 +1250,9 @@ def addAttribute(self,
parentMod = self.currentMod
attr = system.Attribute(system, name, parent)
attr.kind = kind
if kind is model.DocumentableKind.PROPERTY:
# init property info if this attribute is a property
attr.property_def = model.PropertyDef()
attr.parentMod = parentMod
system.addObject(attr)
self.currentAttr = attr
Expand Down
8 changes: 8 additions & 0 deletions pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ def is_using_annotations(expr: Optional[ast.AST],
return True
return False

def iter_decorator_list(decorator_list:Iterable[ast.expr]) -> Iterator[Tuple[Optional[List[str]], ast.expr]]:
for d in decorator_list:
if isinstance(d, ast.Call):
deco_name = node2dottedname(d.func)
else:
deco_name = node2dottedname(d)
yield deco_name,d

def is_none_literal(node: ast.expr) -> bool:
"""Does this AST node represent the literal constant None?"""
return isinstance(node, (ast.Constant, ast.NameConstant)) and node.value is None
Expand Down
Loading