Skip to content

Commit

Permalink
Merge e09ffb3 into 74bc650
Browse files Browse the repository at this point in the history
  • Loading branch information
JWCook committed Jul 3, 2021
2 parents 74bc650 + e09ffb3 commit 661c725
Show file tree
Hide file tree
Showing 20 changed files with 227 additions and 111 deletions.
5 changes: 1 addition & 4 deletions docs/_templates/autosummary_core/class.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
{% if referencefile %}
.. include:: {{ referencefile }}
{% endif %}

{# Automodapi class template for data models #}
{{ objname }}
{{ underline }}

.. currentmodule:: {{ module }}

.. autoclass:: {{ objname }}
:show-inheritance:
:members:

{% block attributes_summary %}
{% if attributes %}
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
sys.path.insert(0, '..')
from pyinaturalist import __version__
from pyinaturalist.constants import DOCS_DIR, PROJECT_DIR, EXAMPLES_DIR, SAMPLE_DATA_DIR
from pyinaturalist.api_docs import document_models
from pyinaturalist.api_docs.model_docs import document_models

# Relevant doc directories used in extension settings
CSS_DIR = join(DOCS_DIR, '_static')
Expand Down
1 change: 0 additions & 1 deletion pyinaturalist/api_docs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@
copy_signatures,
document_request_params,
)
from pyinaturalist.api_docs.model_docs import document_models
54 changes: 44 additions & 10 deletions pyinaturalist/api_docs/model_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
from inspect import getmembers, isclass
from os import makedirs
from os.path import join
from typing import List, Tuple, Type
from typing import Any, List, Tuple, Type, get_type_hints

from attr import Attribute
from sphinx_autodoc_typehints import format_annotation

from pyinaturalist.constants import DOCS_DIR
from pyinaturalist.models import LazyProperty, get_lazy_properties

IGNORE_PROPERTIES = ['row']
MODEL_DOC_DIR = join(DOCS_DIR, 'models')


Expand All @@ -34,23 +37,54 @@ def get_model_classes() -> List[Type]:
return model_classes


# TODO: Also include @properties?
def get_model_doc(cls) -> List[Tuple[str, str, str]]:
"""Get the name, type and description for all model attributes. If an attribute has a validator
with options, include those options in the description.
# TODO: Also include regular @properties?
# TODO: CSS to style LazyProperties with a different background color?
# TODO: Remove autodoc member docs for LazyProperties
def get_model_doc(cls: Type) -> List[Tuple[str, str, str]]:
"""Get the name, type and description for all model attributes, properties, and LazyProperties.
If an attribute has a validator with options, include those options in the description.
"""
return [_get_field_doc(field) for field in cls.__attrs_attrs__ if not field.name.startswith('_')]

doc_rows = [_get_field_doc(field) for field in cls.__attrs_attrs__ if not field.name.startswith('_')]
doc_rows += [('', '', ''), ('', '', '')]
doc_rows += [_get_property_doc(prop) for prop in get_properties(cls)]
doc_rows += [('', '', ''), ('', '', '')]
doc_rows += [_get_lazy_property_doc(prop) for _, prop in get_lazy_properties(cls).items()]
return doc_rows

def _get_field_doc(field: Attribute) -> Tuple[str, str, str]:
from sphinx_autodoc_typehints import format_annotation

field_type = format_annotation(field.type)
def get_properties(cls: Type) -> List[property]:
"""Get all @property descriptors from a class"""
return [
member
for member in cls.__dict__.values()
if isinstance(member, property)
and not isinstance(member, LazyProperty)
and member.fget.__name__ not in IGNORE_PROPERTIES
]


def _get_field_doc(field: Attribute) -> Tuple[str, str, str]:
"""Get a row documenting an attrs Attribute"""
rtype = format_annotation(field.type)
doc = field.metadata.get('doc', '')
if getattr(field.validator, 'options', None):
options = ', '.join([f'``{opt}``' for opt in field.validator.options if opt is not None])
doc += f'\n\n**Options:** {options}'
return (f'**{field.name}**', field_type, doc)
return (f'**{field.name}**', rtype, doc)


def _get_property_doc(prop: property) -> Tuple[str, str, str]:
"""Get a row documenting a regular @property"""
rtype = format_annotation(get_type_hints(prop.fget).get('return', Any))
doc = (prop.fget.__doc__ or '').split('\n')[0]
return (f'**{prop.fget.__name__}**', rtype, doc)


def _get_lazy_property_doc(prop: LazyProperty) -> Tuple[str, str, str]:
"""Get a row documenting a LazyProperty"""
rtype = format_annotation(prop.type)
return (f'**{prop.__name__}**', rtype, prop.__doc__)


def export_model_doc(model_name, doc_table):
Expand Down
9 changes: 7 additions & 2 deletions pyinaturalist/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
from pyinaturalist.constants import JsonResponse, ResponseOrResults
from pyinaturalist.converters import convert_lat_long, try_datetime
from pyinaturalist.models.base import BaseModel, BaseModelCollection, load_json
from pyinaturalist.models.lazy_property import LazyProperty, add_lazy_attrs, get_model_fields
from pyinaturalist.models.lazy_property import (
LazyProperty,
add_lazy_attrs,
get_lazy_properties,
get_model_fields,
)


# Aliases and minor helper functions used by model classes
Expand Down Expand Up @@ -77,13 +82,13 @@ def upper(value) -> Optional[str]:
from pyinaturalist.models.comment import Comment
from pyinaturalist.models.identification import Identification
from pyinaturalist.models.life_list import LifeList, LifeListTaxon
from pyinaturalist.models.observation_field import ObservationField, ObservationFieldValue
from pyinaturalist.models.project import (
Project,
ProjectObservation,
ProjectObservationField,
ProjectUser,
)
from pyinaturalist.models.observation_field import ObservationField, ObservationFieldValue
from pyinaturalist.models.observation import Observation, Observations
from pyinaturalist.models.search import SearchResult

Expand Down
2 changes: 1 addition & 1 deletion pyinaturalist/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
class BaseModel:
"""Base class for data models"""

id: int = field(default=None)
id: int = field(default=None, metadata={'doc': 'Unique record ID'})
temp_attrs: List[str] = []
headers: Dict[str, str] = {}

Expand Down
4 changes: 2 additions & 2 deletions pyinaturalist/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class Comment(BaseModel):
body: str = field(default=None, doc='Comment text')
created_at: datetime = datetime_now_field(doc='Date and time the comment was created')
hidden: bool = field(default=None, doc='Indicates if the comment is hidden')
uuid: str = field(default=None, doc='Universally unique ID')
user: property = LazyProperty(User.from_json)
uuid: str = field(default=None, doc='Universally unique identifier')
user: property = LazyProperty(User.from_json, type=User, doc='User that added the comment')

# Unused attributes
# created_at_details: Dict = field(factory=dict)
Expand Down
9 changes: 6 additions & 3 deletions pyinaturalist/models/controlled_term.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Annotation(BaseModel):
uuid: str = field(default=None)
vote_score: int = field(default=None)
votes: List = field(factory=list)
user: property = LazyProperty(User.from_json)
user: property = LazyProperty(User.from_json, type=User, doc='User who added the annotation')

@property
def values(self) -> List[str]:
Expand Down Expand Up @@ -73,8 +73,11 @@ class ControlledTerm(BaseModel):
uri: str = field(default=None)
uuid: str = field(default=None)
taxon_ids: List[int] = field(factory=list)

values: property = LazyProperty(ControlledTermValue.from_json_list)
values: property = LazyProperty(
ControlledTermValue.from_json_list,
type=List[ControlledTermValue],
doc='Allowed values for this controlled term',
)

@property
def value_labels(self) -> str:
Expand Down
33 changes: 19 additions & 14 deletions pyinaturalist/models/identification.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime

from pyinaturalist.constants import TableRow
from pyinaturalist.constants import ID_CATEGORIES, TableRow
from pyinaturalist.models import (
BaseModel,
LazyProperty,
Expand All @@ -9,6 +9,7 @@
datetime_now_field,
define_model,
field,
is_in,
)


Expand All @@ -18,23 +19,27 @@ class Identification(BaseModel):
`GET /identifications <https://api.inaturalist.org/v1/docs/#!/Identifications/get_identifications>`_.
"""

body: str = field(default=None)
category: str = field(default=None) # Enum
body: str = field(default=None, doc='Comment text')
category: str = field(default=None, validator=is_in(ID_CATEGORIES), doc='Identification category')
created_at: datetime = datetime_now_field(doc='Date and time the identification was added')
current: bool = field(default=None)
current: bool = field(
default=None, doc='Indicates if the identification is the currently accepted one'
)
current_taxon: bool = field(default=None)
disagreement: bool = field(default=None)
disagreement: bool = field(
default=None, doc='Indicates if this identification disagrees with previous ones'
)
hidden: bool = field(default=None)
own_observation: bool = field(default=None)
previous_observation_taxon_id: int = field(default=None)
own_observation: bool = field(default=None, doc='Indicates if the indentifier is also the observer')
previous_observation_taxon_id: int = field(default=None, doc='Previous observation taxon ID')
taxon_change: bool = field(default=None) # TODO: confirm type
taxon_id: int = field(default=None)
uuid: str = field(default=None)
vision: bool = field(default=None)

# Lazy-loaded nested model objects
taxon: property = LazyProperty(Taxon.from_json)
user: property = LazyProperty(User.from_json)
taxon_id: int = field(default=None, doc='Identification taxon ID')
uuid: str = field(default=None, doc='Universally unique identifier')
vision: bool = field(
default=None, doc='Indicates if the taxon was selected from computer vision results'
)
taxon: property = LazyProperty(Taxon.from_json, type=Taxon, doc='Identification taxon')
user: property = LazyProperty(User.from_json, type=User, doc='User that added the indentification')

# Unused attributes
# created_at_details: {}
Expand Down
18 changes: 10 additions & 8 deletions pyinaturalist/models/lazy_property.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from functools import update_wrapper
from inspect import signature
from typing import Any, Callable, Iterable, List
from typing import Any, Callable, Dict, Iterable, List, Type

from attr import Attribute, Factory

Expand All @@ -17,7 +17,6 @@
}


# TODO: Add support for description that will show up in Sphinx docs
class LazyProperty(property):
"""A lazy-initialized/cached descriptor, similar to ``@functools.cached_property``, but works
for slotted classes by not relying on ``__dict__``.
Expand Down Expand Up @@ -50,11 +49,13 @@ class MyModel(BaseModel):
"""

def __init__(self, converter: Callable, name: str = None):
def __init__(self, converter: Callable, name: str = None, doc: str = None, type: Type = BaseModel):
update_wrapper(self, converter)
self.converter = converter
self.default = None
self.type = type
self.__doc__ = doc
self.__set_name__(None, name)
update_wrapper(self, converter)

# Use either a list factory or default value, depending on the converter's return type
if _returns_list(converter):
Expand Down Expand Up @@ -98,15 +99,16 @@ def get_model_fields(obj: Any) -> Iterable[Attribute]:
"""Add placeholder attributes for lazy-loaded model properties so they get picked up by rich's
pretty-printer. Does not change behavior for anything except :py:class:`.BaseModel` subclasses.
"""
from pyinaturalist.models import LazyProperty

attrs = list(obj.__attrs_attrs__)
if isinstance(obj, BaseModel):
prop_names = [k for k, v in type(obj).__dict__.items() if isinstance(v, LazyProperty)]
attrs += [make_attribute(p) for p in prop_names]
attrs += [make_attribute(p) for p in get_lazy_properties(type(obj))]
return attrs


def get_lazy_properties(cls: Type[BaseModel]) -> Dict[str, LazyProperty]:
return {k: v for k, v in cls.__dict__.items() if isinstance(v, LazyProperty)}


def make_attribute(name, **kwargs):
kwargs = {**FIELD_DEFAULTS, **kwargs}
return Attribute(name=name, **kwargs)
Expand Down
6 changes: 4 additions & 2 deletions pyinaturalist/models/life_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
class LifeListTaxon(TaxonCount):
"""A single taxon in a user's life list"""

descendant_obs_count: int = field(default=0)
direct_obs_count: int = field(default=0)
descendant_obs_count: int = field(default=0, doc='Number of observations of taxon children')
direct_obs_count: int = field(
default=0, doc='Number of observations of this exact taxon (excluding children)'
)

@property
def indent_level(self) -> int:
Expand Down
34 changes: 24 additions & 10 deletions pyinaturalist/models/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,33 @@ class Observation(BaseModel):
)
quality_metrics: List[Dict] = field(factory=list, doc='Data quality assessment metrics')
reviewed_by: List[int] = field(factory=list, doc='IDs of users who have reviewed the observation')
sounds: List[Dict] = field(factory=list, doc='Sound files associated with the observation')
sounds: List[Dict] = field(factory=list, doc='Observation sound files')
tags: List[str] = field(factory=list, doc='Arbitrary user tags added to the observation')
votes: List[Dict] = field(factory=list, doc='Votes on data quality assessment metrics')

# Lazy-loaded nested model objects
annotations: property = LazyProperty(Annotation.from_json_list)
comments: property = LazyProperty(Comment.from_json_list)
identifications: property = LazyProperty(Identification.from_json_list)
ofvs: property = LazyProperty(ObservationFieldValue.from_json_list)
photos: property = LazyProperty(Photo.from_json_list)
project_observations: property = LazyProperty(ProjectObservation.from_json_list)
taxon: property = LazyProperty(Taxon.from_json)
user: property = LazyProperty(User.from_json)
# Lazy-loaded model objects
annotations: property = LazyProperty(
Annotation.from_json_list, type=List[Annotation], doc='Observation annotations'
)
comments: property = LazyProperty(
Comment.from_json_list, type=List[Comment], doc='Observation comments'
)
identifications: property = LazyProperty(
Identification.from_json_list, type=List[Identification], doc='Observation identifications'
)
ofvs: property = LazyProperty(
ObservationFieldValue.from_json_list,
type=List[ObservationFieldValue],
doc='Observation field values',
)
photos: property = LazyProperty(Photo.from_json_list, type=List[Photo], doc='Observation photos')
project_observations: property = LazyProperty(
ProjectObservation.from_json_list,
type=List[ProjectObservation],
doc='Details on any projects that the observation has been added to',
)
taxon: property = LazyProperty(Taxon.from_json, type=Taxon, doc='Observation taxon')
user: property = LazyProperty(User.from_json, type=User, doc='Observer')

# Additional attributes from API response that aren't needed; just left here for reference
# cached_votes_total: int = field(default=None)
Expand Down
10 changes: 6 additions & 4 deletions pyinaturalist/models/observation_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ class ObservationFieldValue(BaseModel):
user_id: int = field(default=None)
uuid: str = field(default=None)
value: OFVValue = field(default=None)

# Lazy-loaded nested model objects
taxon: property = LazyProperty(Taxon.from_json)
user: property = LazyProperty(User.from_json)
taxon: property = LazyProperty(
Taxon.from_json, type=Taxon, doc='Taxon that the observation field applies to'
)
user: property = LazyProperty(
User.from_json, type=User, doc='User that applied the observation field value'
)

# Unused attrbiutes
# name_ci: str = field(default=None)
Expand Down
Loading

0 comments on commit 661c725

Please sign in to comment.