From a80f331053fe75026e869759a666d88478da7707 Mon Sep 17 00:00:00 2001 From: Daniel Baston Date: Tue, 27 Aug 2024 21:04:37 -0400 Subject: [PATCH] Python: Improve docstrings and output formatting --- python/doc/conf.py | 21 +++++++-- python/src/exactextract/exact_extract.py | 3 -- python/src/exactextract/feature.py | 26 +++++++---- python/src/exactextract/operation.py | 58 ++++++++++++++++++++---- python/src/exactextract/processor.py | 12 +++-- python/src/exactextract/writer.py | 20 ++++++-- 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/python/doc/conf.py b/python/doc/conf.py index 62602946..b6155109 100644 --- a/python/doc/conf.py +++ b/python/doc/conf.py @@ -4,6 +4,7 @@ # import datetime +import re # -- Project information ----------------------------------------------------- @@ -67,17 +68,27 @@ autoapi_dirs = ["../src/exactextract"] autoapi_keep_files = False autoapi_add_toctree_entry = False +autoapi_python_class_content = "both" # include both class and __init__ docstrings + +autoapi_options = ["members", "undoc-members", "show-module-summary", "special-members"] def autoapi_skip_member(app, what, name, obj, skip, options): - if name.startswith("exactextract.processor"): + + shortname = name.split(".")[-1] + + # Don't emit documentation for anything named beginning with + # an underscore, except for __init__ + if re.match("^_[a-z]", shortname): return True - if name.startswith("exactextract.feature."): + + # Don't emit documentation for member variables, which are + # assumed to be private even if not prefixed with an + # underscore. + if what == "attribute": return True - if name.startswith("_"): - return name != "__init__" - return False + return None # Fallback to default behavior def setup(sphinx): diff --git a/python/src/exactextract/exact_extract.py b/python/src/exactextract/exact_extract.py index ee3a5da0..418fd4a1 100644 --- a/python/src/exactextract/exact_extract.py +++ b/python/src/exactextract/exact_extract.py @@ -177,10 +177,8 @@ def prep_ops(stats, values, weights=None, *, add_unique=False): # these operations to create a set of PythonOperations. nargs = stat.__code__.co_argcount if nargs == 3: - weighted = True dummy_stat = "weighted_sum" elif nargs == 2: - weighted = False dummy_stat = "count" else: raise Exception( @@ -195,7 +193,6 @@ def prep_ops(stats, values, weights=None, *, add_unique=False): op.name.replace(dummy_stat, stat.__name__), op.values, op.weights, - weighted, ) ) except RuntimeError as e: diff --git a/python/src/exactextract/feature.py b/python/src/exactextract/feature.py index d7b1a666..c15acc55 100644 --- a/python/src/exactextract/feature.py +++ b/python/src/exactextract/feature.py @@ -3,6 +3,14 @@ from ._exactextract import Feature from ._exactextract import FeatureSource as _FeatureSource +__all__ = [ + "FeatureSource", + "GDALFeatureSource", + "JSONFeatureSource", + "GeoPandasFeatureSource", + "QGISFeatureSource", +] + class FeatureSource(_FeatureSource): """ @@ -13,6 +21,7 @@ class FeatureSource(_FeatureSource): - :py:class:`GDALFeatureSource` - :py:class:`JSONFeatureSource` - :py:class:`GeoPandasFeatureSource` + - :py:class:`QGISFeatureSource` """ def __init__(self): @@ -26,17 +35,14 @@ class GDALFeatureSource(FeatureSource): def __init__(self, src): """ - Create a GDALFeatureSource. - Args: - src: one of the following: + src: one of the following - string or Path to a file/datasource that can be opened with GDAL/OGR - - a `gdal.Dataset` - - an `ogr.DataSource` - - an `ogr.Layer` - - If the file has more than one layer, e.g., a GeoPackage, an `ogr.Layer` must be provided - directly. + - a ``gdal.Dataset`` + - an ``ogr.DataSource`` + - an ``ogr.Layer`` + If the file has more than one layer, e.g., a GeoPackage, an + ``ogr.Layer`` must be provided directly. """ super().__init__() @@ -265,6 +271,8 @@ def default(self, obj): class QGISFeatureSource(FeatureSource): + """FeatureSource providing features from a QGIS vector layer.""" + def __init__(self, src): super().__init__() self.src = src diff --git a/python/src/exactextract/operation.py b/python/src/exactextract/operation.py index 8c351e52..bf1e2854 100644 --- a/python/src/exactextract/operation.py +++ b/python/src/exactextract/operation.py @@ -1,17 +1,19 @@ from __future__ import annotations -from typing import Mapping, Optional +from typing import Callable, Mapping, Optional from ._exactextract import Operation as _Operation -from ._exactextract import PythonOperation # noqa: F401 +from ._exactextract import PythonOperation as _PythonOperation from ._exactextract import change_stat # noqa: F401 from ._exactextract import prepare_operations # noqa: F401 from .raster import RasterSource +__all__ = ["Operation", "PythonOperation"] + class Operation(_Operation): """ - Summary of pixel values + Summarize of pixel values using a built-in function Defines a summary operation to be performed on pixel values intersecting a geometry. May return a scalar (e.g., ``weighted_mean``), or a @@ -27,13 +29,11 @@ def __init__( options: Optional[Mapping] = None, ): """ - Create an Operation - Args: - stat_name (str): Name of the stat. Refer to docs for options. - field_name (str): Name of the result field that is assigned by this Operation. - raster (RasterSource): Raster to compute over. - weights (Optional[RasterSource], optional): Weight raster to use. Defaults to None. + stat_name: Name of the stat. Refer to docs for options. + field_name: Name of the result field that is assigned by this Operation. + raster: Raster to compute over. + weights: Weight raster to use. Defaults to None. options: Arguments used to control the behavior of an Operation, e.g. ``options={"q": 0.667}`` with ``stat_name = "quantile"`` """ @@ -46,3 +46,43 @@ def __init__( args = {str(k): str(v) for k, v in options.items()} super().__init__(stat_name, field_name, raster, weights, args) + + +class PythonOperation(_PythonOperation): + """ + Summarize of pixel values using a Python function + + Defines a summary operation to be performed on pixel values intersecting + a geometry. + """ + + def __init__( + self, + function: Callable, + field_name: str, + raster: RasterSource, + weights: Optional[RasterSource], + ): + """ + Args: + function: Function accepting either two arguments (if `weights` is `None`), + or three arguments. The function will be called with + arrays of equal length containing: + - pixel values from `raster` (masked array) + - cell coverage fractions + - pixel values from `weights` (masked array) + field_name: Name of the result field that is assigned by this Operation. + raster: Raster to compute over. + weights: Weight raster to use. Defaults to None. + """ + + if raster is None: + raise TypeError + + # Inspect the function to determine if it should be called with + # or without weights. This allows us to pass weights even if + # they are unused, which is important to have weighted and + # unweighted stats using the same common grid. + weighted = function.__code__.co_argcount == 3 + + super().__init__(function, field_name, raster, weights, weighted) diff --git a/python/src/exactextract/processor.py b/python/src/exactextract/processor.py index e198c541..34c1ec8c 100644 --- a/python/src/exactextract/processor.py +++ b/python/src/exactextract/processor.py @@ -7,6 +7,8 @@ from .operation import Operation from .writer import Writer +__all__ = ["FeatureSequentialProcessor", "RasterSequentialProcessor"] + class FeatureSequentialProcessor(_FeatureSequentialProcessor): """Binding class around exactextract FeatureSequentialProcessor""" @@ -19,12 +21,12 @@ def __init__( include_cols: Optional[List[Operation]] = None, ): """ - Create FeatureSequentialProcessor object - Args: ds (FeatureSource): Dataset to use writer (Writer): Writer to use - op_list (List[Operation]): List of operations + op_list: List of Operations to perform + include_cols: List of columns to copy from + input features """ super().__init__(ds, writer) for col in include_cols or []: @@ -44,12 +46,12 @@ def __init__( include_cols: Optional[List[Operation]] = None, ): """ - Create RasterSequentialProcessor object - Args: ds (FeatureSource): Dataset to use writer (Writer): Writer to use op_list (List[Operation]): List of operations + include_cols: List of columns to copy from + input features """ super().__init__(ds, writer) for col in include_cols or []: diff --git a/python/src/exactextract/writer.py b/python/src/exactextract/writer.py index 5cb1b2fa..ac399472 100644 --- a/python/src/exactextract/writer.py +++ b/python/src/exactextract/writer.py @@ -5,6 +5,8 @@ from ._exactextract import Writer as _Writer from .feature import GDALFeature, JSONFeature, QGISFeature +__all__ = ["Writer", "JSONWriter", "PandasWriter", "QGISWriter", "GDALWriter"] + class Writer(_Writer): """Writes the results of summary operations to a desired format""" @@ -25,14 +27,12 @@ def __init__( map_fields: Optional[Mapping[str, Tuple[str]]] = None ): """ - Create a new JSONWriter. - Args: array_type: type that should be used to represent array outputs. - Either "numpy" (default), "list", or "set" - map_fields: An optional dictionary of fields to be created by + either "numpy" (default), "list", or "set" + map_fields: an optional dictionary of fields to be created by interpreting one field as keys and another as values, in the format - ``{ dst_field : (src_keys, src_vals) }``. For example, the fields + ``{ dst_field : (src_keys, src_vals) }``. for example, the fields "values" and "frac" would be combined into a field called "frac_map" using ``map_fields = {"frac_map": ("values", "frac")}``. """ @@ -321,6 +321,16 @@ class GDALWriter(Writer): def __init__( self, dataset=None, *, filename=None, driver=None, layer_name="", srs_wkt=None ): + """ + Args: + dataset: a ``gdal.Dataset`` or ``ogr.DataSource`` to which results + should be created in a new layer + filename: file to write results to, if ``dataset`` is ``None`` + driver: driver to use when creating ``filename`` + layer_name: name of new layer to create in output dataset + srs_wkt: spatial reference system to assign to output dataset. No + coordinate transformation will be performed. + """ super().__init__() from osgeo import gdal, ogr