pandoraaperture 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pandora Data Processing Center
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: pandoraaperture
3
+ Version: 0.1.0
4
+ Summary:
5
+ License-File: LICENSE
6
+ Author: Christina Hedges
7
+ Author-email: christina.l.hedges@nasa.gov
8
+ Requires-Python: >=3.9,<3.13
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: appdirs (>=1.4.4,<2.0.0)
15
+ Requires-Dist: astropy (>=6.0.1)
16
+ Requires-Dist: gaiaoffline (>=1.0.3,<2.0.0)
17
+ Requires-Dist: matplotlib (>=2.0.0)
18
+ Requires-Dist: numpy (>=1.2)
19
+ Requires-Dist: pandas (>=2.2.3,<3.0.0)
20
+ Requires-Dist: pandoraref (>=0.3.0)
21
+ Requires-Dist: rich (>=13.9.4,<14.0.0)
22
+ Requires-Dist: sparse3d (>=1.2.10)
23
+ Description-Content-Type: text/markdown
24
+
25
+ <a href="https://github.com/pandoramission/pandora-aperture/actions/workflows/black.yml"><img src="https://github.com/pandoramission/pandora-aperture/workflows/black/badge.svg" alt="black status"/></a> <a href="https://github.com/pandoramission/pandora-aperture/actions/workflows/flake8.yml"><img src="https://github.com/pandoramission/pandora-aperture/workflows/flake8/badge.svg" alt="flake8 status"/></a> [![Generic badge](https://img.shields.io/badge/documentation-live-blue.svg)](https://pandoramission.github.io/pandora-aperture/)
26
+ [![PyPI - Version](https://img.shields.io/pypi/v/pandoraaperture)](https://pypi.org/project/pandoraaperture/)
27
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pandoraaperture)](https://pypi.org/project/pandoraaperture/)
28
+
29
+ # Pandora Aperture
30
+
31
+ ## Installation
32
+
33
+ To install you can use
34
+
35
+ ```sh
36
+ pip install pandoraaperture --upgrade
37
+ ```
38
+
39
+ You should update your package often, as we frequently put out new versions with updated Current Best Estimates, and some limited new functionality. Check your version number using
40
+
41
+ ```python
42
+ import pandoraaperture as pa
43
+ pa.__version__
44
+ ```
45
+
@@ -0,0 +1,20 @@
1
+ <a href="https://github.com/pandoramission/pandora-aperture/actions/workflows/black.yml"><img src="https://github.com/pandoramission/pandora-aperture/workflows/black/badge.svg" alt="black status"/></a> <a href="https://github.com/pandoramission/pandora-aperture/actions/workflows/flake8.yml"><img src="https://github.com/pandoramission/pandora-aperture/workflows/flake8/badge.svg" alt="flake8 status"/></a> [![Generic badge](https://img.shields.io/badge/documentation-live-blue.svg)](https://pandoramission.github.io/pandora-aperture/)
2
+ [![PyPI - Version](https://img.shields.io/pypi/v/pandoraaperture)](https://pypi.org/project/pandoraaperture/)
3
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pandoraaperture)](https://pypi.org/project/pandoraaperture/)
4
+
5
+ # Pandora Aperture
6
+
7
+ ## Installation
8
+
9
+ To install you can use
10
+
11
+ ```sh
12
+ pip install pandoraaperture --upgrade
13
+ ```
14
+
15
+ You should update your package often, as we frequently put out new versions with updated Current Best Estimates, and some limited new functionality. Check your version number using
16
+
17
+ ```python
18
+ import pandoraaperture as pa
19
+ pa.__version__
20
+ ```
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "pandoraaperture"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = [
6
+ {name = "Christina Hedges",email = "christina.l.hedges@nasa.gov"}
7
+ ]
8
+ readme = "docs/README.md"
9
+ requires-python = ">=3.9,<3.13"
10
+ dependencies = [
11
+ "rich (>=13.9.4,<14.0.0)",
12
+ "numpy (>=1.2)",
13
+ "pandas (>=2.2.3,<3.0.0)",
14
+ "appdirs (>=1.4.4,<2.0.0)",
15
+ "sparse3d (>=1.2.10)",
16
+ "matplotlib (>=2.0.0)",
17
+ "astropy (>=6.0.1)",
18
+ "gaiaoffline (>=1.0.3,<2.0.0)",
19
+ "pandoraref (>=0.3.0)"
20
+ ]
21
+
22
+ [tool.poetry]
23
+ packages = [{include = "pandoraaperture", from = "src"}]
24
+
25
+ [tool.poetry.group.dev]
26
+ optional = true
27
+
28
+ [tool.poetry.group.dev.dependencies]
29
+ pytest = "^8.3.4"
30
+ black = "^25.1.0"
31
+ isort = "^6.0.0"
32
+ flake8 = "^7.1.2"
33
+ jupyterlab = "^4.3.5"
34
+ mkdocs = "^1.6.1"
35
+ mkdocs-jupyter = "^0.25.1"
36
+ mkdocs-material = "^9.6.5"
37
+ pytkdocs = {version = "^0.16.2", extras = ["numpy-style"]}
38
+ mkdocs-include-markdown-plugin = "^7.1.4"
39
+ mkdocstrings = {version = "^0.28.2", extras = ["python"]}
40
+ jupyter-contrib-nbextensions = "^0.7.0"
41
+ notebook = ">=6.0.0,<7.0.0"
42
+
43
+ [build-system]
44
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
45
+ build-backend = "poetry.core.masonry.api"
46
+
47
+ [tool.black]
48
+ line-length = 79
49
+
50
+ [tool.isort]
51
+ import_heading_firstparty = 'First-party/Local'
52
+ import_heading_future = 'Future'
53
+ import_heading_stdlib = 'Standard library'
54
+ import_heading_thirdparty = 'Third-party'
55
+ line_length = 79
56
+ multi_line_output = 3
57
+ no_lines_before = 'LOCALFOLDER'
58
+ profile = 'black'
@@ -0,0 +1,149 @@
1
+ # Standard library
2
+ import logging # noqa: E402
3
+ import os # noqa
4
+ from glob import glob
5
+
6
+ # Third-party
7
+ from rich.console import Console # noqa: E402
8
+ from rich.logging import RichHandler # noqa: E402
9
+
10
+ PACKAGEDIR = os.path.abspath(os.path.dirname(__file__))
11
+ DOCSDIR = "/".join(PACKAGEDIR.split("/")[:-2]) + "/docs/"
12
+ TESTDIR = "/".join(PACKAGEDIR.split("/")[:-2]) + "/tests/"
13
+ PANDORASTYLE = glob(f"{PACKAGEDIR}/data/pandora.mplstyle")
14
+
15
+ # Standard library
16
+ import configparser # noqa: E402
17
+ from importlib.metadata import PackageNotFoundError, version # noqa
18
+
19
+ # Third-party
20
+ import numpy as np # noqa: E402
21
+ import pandas as pd # noqa: E402
22
+ import pandoraref as pr # noqa: E402
23
+ from appdirs import user_config_dir, user_data_dir # noqa: E402
24
+
25
+
26
+ def get_version():
27
+ try:
28
+ return version("pandoraaperture")
29
+ except PackageNotFoundError:
30
+ return "unknown"
31
+
32
+
33
+ __version__ = get_version()
34
+
35
+
36
+ # Custom Logger with Rich
37
+ class PandoraLogger(logging.Logger):
38
+ def __init__(self, name, level=logging.INFO):
39
+ super().__init__(name, level)
40
+ console = Console()
41
+ self.handler = RichHandler(
42
+ show_time=False, show_level=False, show_path=False, console=console
43
+ )
44
+ self.handler.setFormatter(
45
+ logging.Formatter(
46
+ "%(asctime)s %(levelname)s: %(message)s",
47
+ datefmt="%Y-%m-%d %H:%M:%S",
48
+ )
49
+ )
50
+ self.addHandler(self.handler)
51
+
52
+
53
+ def get_logger(name="pandoraaperture"):
54
+ """Configure and return a logger with RichHandler."""
55
+ return PandoraLogger(name)
56
+
57
+
58
+ CONFIGDIR = user_config_dir("pandoraaperture")
59
+ os.makedirs(CONFIGDIR, exist_ok=True)
60
+ CONFIGPATH = os.path.join(CONFIGDIR, "config.ini")
61
+
62
+ logger = get_logger("pandoraaperture")
63
+
64
+
65
+ def reset_config():
66
+ """Set the config to defaults."""
67
+ # use this function to set your default configuration parameters.
68
+ config = configparser.ConfigParser()
69
+ config["SETTINGS"] = {
70
+ "log_level": "WARNING",
71
+ "data_dir": user_data_dir("pandoraaperture"),
72
+ "pixel_buffer": 15,
73
+ "catalog_columns": "source_id, phot_g_mean_flux, phot_bp_mean_flux, phot_rp_mean_flux, j_flux, h_flux, k_flux, teff_gspphot",
74
+ }
75
+ with open(CONFIGPATH, "w") as configfile:
76
+ config.write(configfile)
77
+
78
+
79
+ def load_config() -> configparser.ConfigParser:
80
+ """
81
+ Loads the configuration file, creating it with defaults if it doesn't exist.
82
+
83
+ Returns
84
+ -------
85
+ configparser.ConfigParser
86
+ The loaded configuration.
87
+ """
88
+
89
+ config = configparser.ConfigParser()
90
+
91
+ if not os.path.exists(CONFIGPATH):
92
+ # Create default configuration
93
+ reset_config()
94
+ config.read(CONFIGPATH)
95
+ return config
96
+
97
+
98
+ def save_config(config: configparser.ConfigParser) -> None:
99
+ """
100
+ Saves the configuration to the file.
101
+
102
+ Parameters
103
+ ----------
104
+ config : configparser.ConfigParser
105
+ The configuration to save.
106
+ app_name : str
107
+ Name of the application.
108
+ """
109
+ with open(CONFIGPATH, "w") as configfile:
110
+ config.write(configfile)
111
+
112
+
113
+ config = load_config()
114
+
115
+ # Use this to check that keys you expect are in the config file.
116
+ # If you update the config file and think users may be out of date
117
+ # add the config parameters to this loop to check and reset the config.
118
+ for key in ["data_dir", "log_level"]:
119
+ if key not in config["SETTINGS"]:
120
+ logger.error(
121
+ f"`{key}` missing from the `pandoraaperture` config file. Your configuration is being reset."
122
+ )
123
+ reset_config()
124
+ config = load_config()
125
+
126
+ DATADIR = config["SETTINGS"]["data_dir"]
127
+ logger.setLevel(config["SETTINGS"]["log_level"])
128
+
129
+
130
+ def display_config() -> pd.DataFrame:
131
+ dfs = []
132
+ for section in config.sections():
133
+ df = pd.DataFrame(
134
+ np.asarray(
135
+ [(key, value) for key, value in dict(config[section]).items()]
136
+ )
137
+ )
138
+ df["section"] = section
139
+ df.columns = ["key", "value", "section"]
140
+ df = df.set_index(["section", "key"])
141
+ dfs.append(df)
142
+ return pd.concat(dfs)
143
+
144
+
145
+ NIRDAReference = pr.NIRDAReference()
146
+ VISDAReference = pr.VISDAReference()
147
+
148
+ from .prf import PRF, DispersedPRF, SpatialPRF # noqa: E402,F401
149
+ from .scene import DispersedSkyScene, ROISkyScene, SkyScene # noqa: E402,F401
@@ -0,0 +1,71 @@
1
+ lines.color: C0 # has no affect on plot(); see axes.prop_cycle
2
+ lines.linewidth : 0.8
3
+ lines.markersize : 1.5
4
+ figure.facecolor : white
5
+ figure.edgecolor: white # figure edge color
6
+ figure.dpi : 150
7
+ figure.titlesize: x-large # size of the figure title (``Figure.suptitle()``)
8
+ figure.autolayout: True # When True, automatically adjust subplot
9
+ # parameters to make the plot fit the figure
10
+ # using `tight_layout`
11
+ image.origin: lower # {lower, upper}
12
+ text.color : 000000
13
+ axes.spines.top : False
14
+ axes.spines.right : False
15
+ axes.titlepad : 10
16
+ axes.titlesize : x-large
17
+ axes.facecolor : white
18
+ axes.edgecolor : 000000
19
+ axes.labelcolor : 000000
20
+ axes.labelsize : large
21
+ axes.linewidth : 1.25
22
+ axes.prop_cycle : cycler('color', ['000000', '0072B2', '009E73', 'D55E00', 'CC79A7', 'F0E442', '56B4E9'])
23
+ axes.formatter.use_mathtext: True # When True, use mathtext for scientific
24
+ # notation.
25
+ axes.formatter.min_exponent: 1 # minimum exponent to format in scientific notation
26
+ axes.formatter.useoffset: True # If True, the tick label formatter
27
+ # will default to labeling ticks relative
28
+ # to an offset when the data range is
29
+ # small compared to the minimum absolute
30
+ # value of the data.
31
+ axes.formatter.offset_threshold: 4 # When useoffset is True, the offset
32
+ # will be used when it can remove
33
+ # at least this number of significant
34
+ # digits from tick labels.
35
+
36
+ axes.unicode_minus: True # use Unicode for the minus symbol rather than hyphen. See
37
+ # https://en.wikipedia.org/wiki/Plus_and_minus_signs#Character_codes
38
+ axes.labelpad: 7.0
39
+ errorbar.capsize: 3
40
+ legend.fontsize : medium
41
+ legend.frameon : True
42
+ legend.numpoints : 3
43
+ legend.scatterpoints : 3
44
+ legend.facecolor : inherit
45
+ legend.edgecolor : 000000
46
+ legend.framealpha: 0.9
47
+ legend.handlelength: 2.5 # the length of the legend lines
48
+
49
+
50
+ savefig.dpi: 150 # figure dots per inch or 'figure'
51
+ savefig.bbox: tight # {tight, standard}
52
+
53
+ grid.linestyle: --
54
+
55
+ xtick.top : False
56
+ xtick.color : 666666
57
+ xtick.labelcolor : 000000
58
+ xtick.direction : in
59
+ xtick.major.size : 8
60
+ xtick.minor.size : 4
61
+ xtick.minor.visible : True
62
+ xtick.labelsize : medium
63
+
64
+ ytick.minor.visible : True
65
+ ytick.right : False
66
+ ytick.color : 666666
67
+ ytick.labelcolor : 000000
68
+ ytick.direction : in
69
+ ytick.major.size : 8
70
+ ytick.minor.size : 4
71
+ ytick.labelsize : medium
@@ -0,0 +1,239 @@
1
+ # Standard library
2
+ import functools
3
+
4
+ # Third-party
5
+ import astropy.units as u
6
+ import numpy.typing as npt
7
+ import pandas as pd
8
+ from astropy.coordinates import SkyCoord
9
+ from astropy.time import Time
10
+ from astropy.wcs import WCS
11
+ from scipy.interpolate import RectBivariateSpline
12
+ from sparse3d import Sparse3D
13
+
14
+ DOCSTRINGS = {
15
+ "name": (str, "Name of detector, choose from VISDA or NIRDA."),
16
+ "delta_pos": (
17
+ tuple,
18
+ "Change in position in pixels. Use format (row, column).",
19
+ ),
20
+ "file": (
21
+ str,
22
+ "Input file to use. Choose either a string path to the file or an `astropy.fits.HDUList` object",
23
+ ),
24
+ "flux": (
25
+ npt.NDArray,
26
+ "Array of the flux of the PRF as a function of position. Usually normalized such that the total flux is 1.",
27
+ ),
28
+ "gradients": (
29
+ bool,
30
+ "Whether to return gradients. If True, will return an additional 2 arrays that contain the gradients in each axis.",
31
+ ),
32
+ "pixel_size": (
33
+ u.Quantity,
34
+ "True detector pixel size in dimensions of length/pixel",
35
+ ),
36
+ "sub_pixel_size": (
37
+ u.Quantity,
38
+ "PSF file pixel size in dimensions of length/pixel",
39
+ ),
40
+ "scale": (
41
+ float,
42
+ "How much to scale the PRF by. Scale of 2 makes the PSF 2x broader. Default is 1.",
43
+ ),
44
+ "imshape": (
45
+ tuple,
46
+ "Tuple of the shape of the true image. "
47
+ "If using ROIs, use the shape of the image that "
48
+ "each ROI is cut out from. Use format (row, column).",
49
+ ),
50
+ "imcorner": (
51
+ tuple,
52
+ "Tuple of the lower left corner of the image, i.e. it's origin. "
53
+ "Use this to move the image around on the grid. If using a window mode,"
54
+ " make sure to set this to the right corner. Use format (row, column).",
55
+ ),
56
+ "location": (
57
+ tuple,
58
+ "Location of the source on the detector. Use format (row, column). "
59
+ "If not set will default to `self._default_location` which is in the "
60
+ "middle of the image as set by `imshape` and `imcorner`.",
61
+ ),
62
+ "spline": (RectBivariateSpline, "A spline model describing the PRF."),
63
+ "normalize": (
64
+ bool,
65
+ "Whether to normalize the input data so that the total flux is 1.",
66
+ ),
67
+ "row_im": (
68
+ npt.NDArray,
69
+ "1D Array of integer row values at which the PRF is evaluated.",
70
+ ),
71
+ "column_im": (
72
+ npt.NDArray,
73
+ "1D Array of integer column values at which the PRF is evaluated.",
74
+ ),
75
+ "prf_im": (
76
+ npt.NDArray,
77
+ "2D Array of PRF values.",
78
+ ),
79
+ "dprf_im": (
80
+ npt.NDArray,
81
+ "2, 2D arrays of the gradient of the PRF values. Only returned if `gradients`=True.",
82
+ ),
83
+ "X": (Sparse3D, "Sparse3D object containing the PRF at a given location."),
84
+ "dX0": (
85
+ Sparse3D,
86
+ "Sparse3D object containing the gradient of the PRF in axis 0"
87
+ " at a given location. Only returned if `gradients`=True.",
88
+ ),
89
+ "dX1": (
90
+ Sparse3D,
91
+ "Sparse3D object containing the gradient of the PRF in axis 1"
92
+ " at a given location. Only returned if `gradients`=True.",
93
+ ),
94
+ "focal_row": (
95
+ u.Quantity,
96
+ "Row position on the focal plane that corresponds to each element in the flux array. Units of pixels.",
97
+ ),
98
+ "focal_column": (
99
+ u.Quantity,
100
+ "Column position on the focal plane that corresponds to each element in the flux array. Units of pixels.",
101
+ ),
102
+ "trace_row": (
103
+ u.Quantity,
104
+ "Row position within the spectral trace that corresponds to each element in the flux array. Units of pixels.",
105
+ ),
106
+ "trace_column": (
107
+ u.Quantity,
108
+ "Column position within the spectral trace that corresponds to each element in the flux array. Units of pixels.",
109
+ ),
110
+ "prf": ("PRF", "pandoraaperture PRF class."),
111
+ "wcs": (WCS, "astropy World Coordinate System"),
112
+ "time": (Time, "astropy Time"),
113
+ "user_cat": (
114
+ pd.DataFrame,
115
+ "Optional catalog from the user. Use this to pass a catalog of objects expected in this"
116
+ " data that are not part of the Gaia catalog. You must include all the columns "
117
+ "specified in your config file under `catalog_columns`.",
118
+ ),
119
+ "nROIs": (int, "The number of regions of interest in the larger image"),
120
+ "ROI_size": (
121
+ tuple,
122
+ "The size the regions of interest in (row, column) pixels. All ROIs must be the same size.",
123
+ ),
124
+ "ROI_corners": (
125
+ list,
126
+ "The origin (lower left) corner positon for each of the ROIs. Must have length nROIs. List of tuples.",
127
+ ),
128
+ "cat": (
129
+ pd.DataFrame,
130
+ "Catalog of sources that will land within the image.",
131
+ ),
132
+ "coord": (SkyCoord, "Coordinate in the sky."),
133
+ "radius": (u.Quantity, "A radius in degrees for a cone search."),
134
+ "A": (
135
+ Sparse3D,
136
+ "Matrix containing the PRFs of all targets in the scene. ",
137
+ ),
138
+ "target": (
139
+ int,
140
+ "Indicates a target in the catalog. Use either in integer to express "
141
+ "an index in the catalog, or a SkyCoord to find the closest target",
142
+ ),
143
+ "threshold": (
144
+ float,
145
+ "Threshold to cut aperture off at, in units of electrons/s.",
146
+ ),
147
+ "ra": (u.Quantity, "Right Ascention"),
148
+ "dec": (u.Quantity, "Declination"),
149
+ "theta": (u.Quantity, "Roll angle in degrees"),
150
+ "pixel_resolution": (
151
+ float,
152
+ "The separation between different elements in the PRF model in pixels. For example 0.25"
153
+ " means there will be approximately 0.25 pixels between each element.",
154
+ ),
155
+ }
156
+
157
+
158
+ def extract_docstring_type(dtype, desc):
159
+ if isinstance(dtype, tuple):
160
+ dtype_str = " or ".join(
161
+ [t._name if hasattr(t, "_name") else t.__name__][0]
162
+ for t in dtype
163
+ if t is not None
164
+ )
165
+ dtype_str += " or None" if None in dtype else ""
166
+ elif isinstance(dtype, str):
167
+ dtype_str = dtype
168
+ else:
169
+ dtype_str = dtype.__name__
170
+ return dtype_str, desc
171
+
172
+
173
+ def clean_docstring(func, additional_docstring, indent_str, heading):
174
+ existing_docstring = func.__doc__ or ""
175
+ if heading in existing_docstring:
176
+ func.__doc__ = (
177
+ existing_docstring.split("---\n")[0]
178
+ + "---\n"
179
+ + additional_docstring
180
+ + "---\n".join(existing_docstring.split("---\n")[1:])
181
+ )
182
+ else:
183
+ func.__doc__ = (
184
+ existing_docstring
185
+ + f"\n\n{indent_str}{heading}\n{indent_str}----------\n"
186
+ + additional_docstring
187
+ )
188
+
189
+
190
+ # Decorator to add common parameters to docstring
191
+ def add_docstring(func=None, *, parameters=None, returns=None):
192
+ def decorator(func, parameters=parameters, returns=returns):
193
+ param_docstring, return_docstring = "", ""
194
+ if func.__doc__:
195
+ # Determine the current indentation level
196
+ lines = func.__doc__.splitlines()
197
+ if len(lines[0]) == 0:
198
+ indent = len(lines[1]) - len(lines[1].lstrip())
199
+ else:
200
+ indent = len(lines[0]) - len(lines[0].lstrip())
201
+ else:
202
+ indent = 0
203
+ indent_str = " " * indent
204
+ if isinstance(parameters, str):
205
+ parameters = [parameters]
206
+ if isinstance(returns, str):
207
+ returns = [returns]
208
+
209
+ if parameters is not None:
210
+ for name in parameters:
211
+ if name in DOCSTRINGS:
212
+ dtype_str, desc = extract_docstring_type(*DOCSTRINGS[name])
213
+ param_docstring += f"{indent_str}{name}: {dtype_str}\n{indent_str} {desc}\n"
214
+ clean_docstring(func, param_docstring, indent_str, "Parameters")
215
+
216
+ if returns is not None:
217
+ for name in returns:
218
+ if name in DOCSTRINGS:
219
+ dtype_str, desc = extract_docstring_type(*DOCSTRINGS[name])
220
+ return_docstring += f"{indent_str}{name}: {dtype_str}\n{indent_str} {desc}\n"
221
+ clean_docstring(func, return_docstring, indent_str, "Returns")
222
+ return func
223
+
224
+ return decorator
225
+
226
+
227
+ # Decorator to inherit docstring from base class
228
+ def inherit_docstring(func):
229
+ @functools.wraps(func)
230
+ def wrapper(*args, **kwargs):
231
+ return func(*args, **kwargs)
232
+
233
+ if func.__doc__ is None:
234
+ for base in func.__qualname__.split(".")[0].__bases__:
235
+ base_func = getattr(base, func.__name__, None)
236
+ if base_func and base_func.__doc__:
237
+ func.__doc__ = base_func.__doc__
238
+ break
239
+ return wrapper