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

Make titles of the docstrings more useful (plus cleanup) #47

Merged
merged 3 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion erfa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

from .core import * # noqa
from .ufunc import (dt_eraASTROM, dt_eraLDBODY, dt_eraLEAPSECOND, # noqa
dt_pv, dt_sign, dt_type, dt_ymdf, dt_hmsf, dt_dmsf)
dt_pv, dt_sign, dt_type, dt_ymdf, dt_hmsf, dt_dmsf)
from .helpers import leap_seconds # noqa
from .version import version as __version__ # noqa
89 changes: 52 additions & 37 deletions erfa/core.py.templ
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ import numpy

from . import ufunc

__all__ = ['ErfaError', 'ErfaWarning',
{{ funcs|map(attribute='pyname')|surround("'","'")|join(", ") }},
{{ constants|map(attribute='name')|surround("'","'")|join(", ") }}]
__all__ = [
'ErfaError', 'ErfaWarning',
{{ funcs | map(attribute='pyname') | surround("'","'")
| join(", ") | wordwrap(wrapstring='\n ') }},
{{ constants | map(attribute='name') | surround("'","'")
| join(", ") | wordwrap(wrapstring='\n ') }}]


class ErfaError(ValueError):
Expand Down Expand Up @@ -78,41 +81,48 @@ def check_errwarn(statcodes, func_name):
statcodes[statcodes == before] = after
STATUS_CODES[func_name][after] = STATUS_CODES[func_name][before]

if numpy.any(statcodes<0):
# errors present - only report the errors.
if numpy.any(statcodes < 0):
# Errors present - only report the errors.
if statcodes.shape:
statcodes = statcodes[statcodes<0]
statcodes = statcodes[statcodes < 0]

errcodes = numpy.unique(statcodes)

errcounts = dict([(e, numpy.sum(statcodes==e)) for e in errcodes])
errcounts = dict([(e, numpy.sum(statcodes == e)) for e in errcodes])

elsemsg = STATUS_CODES[func_name].get('else', None)
if elsemsg is None:
errmsgs = dict([(e, STATUS_CODES[func_name].get(e, 'Return code ' + str(e))) for e in errcodes])
errmsgs = dict([(e, STATUS_CODES[func_name].get(
e, 'Return code ' + str(e))) for e in errcodes])
else:
errmsgs = dict([(e, STATUS_CODES[func_name].get(e, elsemsg)) for e in errcodes])
errmsgs = dict([(e, STATUS_CODES[func_name].get(
e, elsemsg)) for e in errcodes])

emsg = ', '.join(['{0} of "{1}"'.format(errcounts[e], errmsgs[e]) for e in errcodes])
emsg = ', '.join(['{0} of "{1}"'.format(errcounts[e], errmsgs[e])
for e in errcodes])
raise ErfaError('ERFA function "' + func_name + '" yielded ' + emsg)

elif numpy.any(statcodes>0):
#only warnings present
elif numpy.any(statcodes > 0):
# Only warnings present.
if statcodes.shape:
statcodes = statcodes[statcodes>0]
statcodes = statcodes[statcodes > 0]

warncodes = numpy.unique(statcodes)

warncounts = dict([(w, numpy.sum(statcodes==w)) for w in warncodes])
warncounts = dict([(w, numpy.sum(statcodes == w)) for w in warncodes])

elsemsg = STATUS_CODES[func_name].get('else', None)
if elsemsg is None:
warnmsgs = dict([(w, STATUS_CODES[func_name].get(w, 'Return code ' + str(w))) for w in warncodes])
warnmsgs = dict([(w, STATUS_CODES[func_name].get(
w, 'Return code ' + str(w))) for w in warncodes])
else:
warnmsgs = dict([(w, STATUS_CODES[func_name].get(w, elsemsg)) for w in warncodes])
warnmsgs = dict([(w, STATUS_CODES[func_name].get(
w, elsemsg)) for w in warncodes])

wmsg = ', '.join(['{0} of "{1}"'.format(warncounts[w], warnmsgs[w]) for w in warncodes])
warnings.warn('ERFA function "' + func_name + '" yielded ' + wmsg, ErfaWarning)
wmsg = ', '.join(['{0} of "{1}"'.format(warncounts[w], warnmsgs[w])
for w in warncodes])
warnings.warn('ERFA function {!r} yielded {}'.format(func_name, wmsg),
ErfaWarning)


# <------------------------structured dtype conversion------------------------>
Expand All @@ -124,14 +134,16 @@ dt_bytes12 = numpy.dtype('S12')

{% for constant in constants %}
{{ constant.name }} = {{ constant.value }}
"""{{ constant.doc|join(' ') }}"""
"""{{ constant.doc|join(' ')|wordwrap() }}"""
{%- endfor %}


{% for func in funcs -%}
{%- for func in funcs %}


def {{ func.pyname }}({{ func.args_by_inout('in|inout')|map(attribute='name')|join(', ') }}):
"""
Wrapper for ERFA function ``{{ func.name }}``.
{{ func.doc.title }}

Parameters
----------
Expand All @@ -147,14 +159,11 @@ def {{ func.pyname }}({{ func.args_by_inout('in|inout')|map(attribute='name')|jo

Notes
-----
The ERFA documentation is below. {% if func.args_by_inout('inout') -%}
Note that, unlike the erfa routine,
Wraps ERFA function ``{{ func.name }}``.
{%- if func.args_by_inout('inout') %} Note that, unlike the erfa routine,
the python wrapper does not change {{ func.args_by_inout('inout')
| map(attribute='name')|join(', ') }} in-place.
{%- endif %}


::
{%- endif %} The ERFA documentation is::

{{ func.doc }}
"""
Expand Down Expand Up @@ -183,15 +192,21 @@ def {{ func.pyname }}({{ func.args_by_inout('in|inout')|map(attribute='name')|jo
#}
return {{ func.args_by_inout('inout|out|ret')|map(attribute='name')|join(', ') }}


{#
{#-
# Define the status codes that this function returns.
#}
{%- if func.args_by_inout('stat') -%}
{%- for stat in func.args_by_inout('stat') -%}
{%- if stat.doc_info.statuscodes -%}
STATUS_CODES['{{ func.pyname }}'] = {{ stat.doc_info.statuscodes|string }}
{% endif %}
{% endfor %}
{% endif -%}
{% endfor -%}
{%- if func.args_by_inout('stat') %}
{%- for stat in func.args_by_inout('stat') %}
{%- if stat.doc_info.statuscodes %}


STATUS_CODES['{{ func.pyname }}'] = {
{%- for key, value in stat.doc_info.statuscodes.items() %}
{{ '{!r}'.format(key) }}: {{ '{!r}'.format(value) }},
{%- endfor %}
}
{%- endif %}
{%- endfor %}
{%- endif %}
{%- endfor %}
{# done! (note: this comment also ensures final new line!) #}
15 changes: 11 additions & 4 deletions erfa/tests/test_erfa.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,19 @@ def test_erfa_wrapper():
ra = np.linspace(0.0, np.pi*2.0, 5)
dec = np.linspace(-np.pi/2.0, np.pi/2.0, 4)

aob, zob, hob, dob, rob, eo = erfa.atco13(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, jd, 0.0, 0.0, 0.0, np.pi/4.0, 0.0, 0.0, 0.0, 1014.0, 0.0, 0.0, 0.5)
aob, zob, hob, dob, rob, eo = erfa.atco13(
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, jd,
0.0, 0.0, 0.0, np.pi/4.0, 0.0, 0.0, 0.0, 1014.0, 0.0, 0.0, 0.5)
assert aob.shape == (121,)

aob, zob, hob, dob, rob, eo = erfa.atco13(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, jd[0], 0.0, 0.0, 0.0, np.pi/4.0, 0.0, 0.0, 0.0, 1014.0, 0.0, 0.0, 0.5)
aob, zob, hob, dob, rob, eo = erfa.atco13(
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, jd[0],
0.0, 0.0, 0.0, np.pi/4.0, 0.0, 0.0, 0.0, 1014.0, 0.0, 0.0, 0.5)
assert aob.shape == ()

aob, zob, hob, dob, rob, eo = erfa.atco13(ra[:, None, None], dec[None, :, None], 0.0, 0.0, 0.0, 0.0, jd[None, None, :], 0.0, 0.0, 0.0, np.pi/4.0, 0.0, 0.0, 0.0, 1014.0, 0.0, 0.0, 0.5)
aob, zob, hob, dob, rob, eo = erfa.atco13(
ra[:, None, None], dec[None, :, None], 0.0, 0.0, 0.0, 0.0, jd[None, None, :],
0.0, 0.0, 0.0, np.pi/4.0, 0.0, 0.0, 0.0, 1014.0, 0.0, 0.0, 0.5)
(aob.shape) == (5, 4, 121)

iy, im, id, ihmsf = erfa.d2dtf("UTC", 3, jd, 0.0)
Expand Down Expand Up @@ -167,7 +173,8 @@ def test_errwarn_reporting():
erfa.dat(200, [1, 34, 2], [1, 1, 43], 0.5)
except erfa.ErfaError as e:
if 'warning' in e.args[0]:
assert False, 'Raised the correct type of error, but there were warnings mixed in: ' + e.args[0]
assert False, ('Raised the correct type of error, but there were '
'warnings mixed in: ' + e.args[0])


def test_vector_inouts():
Expand Down
6 changes: 3 additions & 3 deletions erfa/ufunc.c.templ
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@
"Returns\n" \
"-------\n" \
"leap_seconds : `~numpy.ndarray`\n" \
" With structured dtype `~astropy._erfa.dt_eraLEAPSECOND`,\n" \
" With structured dtype `~erfa.dt_eraLEAPSECOND`,\n" \
" containing items 'year', 'month', and 'tai_utc'."
#define SET_LEAP_SECONDS_DOCSTRING \
"set_leap_seconds([table])\n\n" \
"Set the leap second table used in ERFA.\n\n" \
"Parameters\n" \
"----------\n" \
"leap_seconds : array_like, optional\n" \
" With structured dtype `~astropy._erfa.dt_eraLEAPSECOND`,\n" \
" With structured dtype `~erfa.dt_eraLEAPSECOND`,\n" \
" containing items 'year', 'month', and 'tai_utc'.\n" \
" If not given, reset to the ERFA built-in table.\n\n" \
"Notes\n" \
Expand Down Expand Up @@ -751,7 +751,7 @@ PyMODINIT_FUNC PyInit_ufunc(void)
* Make the version information available in the module.
* Note that this gets run every time _erfa is imported,
* hence if the library is provided by the system rather
* than bundled with astropy, one correctly gets the version
* than bundled with pyerfa, one correctly gets the version
* information from the system library.
*/
erfa_version = PyUnicode_FromString(eraVersion());
Expand Down
78 changes: 57 additions & 21 deletions erfa_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
DEFAULT_ERFA_LOC = os.path.join(os.path.split(__file__)[0], 'liberfa/erfa/src')
DEFAULT_TEMPLATE_LOC = os.path.join(os.path.split(__file__)[0], 'erfa')

NDIMS_REX = re.compile(re.escape("numpy.dtype([('fi0', '.*', <(.*)>)])").replace(r'\.\*', '.*').replace(r'\<', '(').replace(r'\>', ')'))
NDIMS_REX = re.compile(re.escape("numpy.dtype([('fi0', '.*', <(.*)>)])")
.replace(r'\.\*', '.*')
.replace(r'\<', '(')
.replace(r'\>', ')'))


class FunctionDoc:
Expand Down Expand Up @@ -101,7 +104,8 @@ def output(self):
def ret_info(self):
if self.__ret_info is None:
ret_info = []
result = re.search("Returned \\(function value\\)([^\n]*):\n(.+?) \n", self.doc, re.DOTALL)
result = re.search("Returned \\(function value\\)([^\n]*):\n(.+?) \n",
self.doc, re.DOTALL)
if result is not None:
ret_info.append(ReturnDoc(result.group(2)))

Expand All @@ -110,12 +114,32 @@ def ret_info(self):
elif len(ret_info) == 1:
self.__ret_info = ret_info[0]
else:
raise ValueError("Multiple C return sections found in this doc:\n" + self.doc)
raise ValueError("Multiple C return sections found in this doc:\n"
+ self.doc)

return self.__ret_info

@property
def title(self):
# Used for the docstring title.
lines = [line.strip() for line in self.doc.split('\n')[4:10]]
# Always include the first line, then stop at either an empty
# line or at the end of a sentence.
description = lines[:1]
for line in lines[1:]:
if line == '':
break
if '. ' in line:
line = line[:line.index('. ')+1]
description.append(line)
if line.endswith('.'):
break

return '\n '.join(description)

def __repr__(self):
return self.doc.replace(" \n", "\n")
return '\n'.join([(ln.rstrip() if ln.strip() else '')
for ln in self.doc.split('\n')])


class ArgumentDoc:
Expand All @@ -124,7 +148,7 @@ def __init__(self, doc):
match = re.search("^ +([^ ]+)[ ]+([^ ]+)[ ]+(.+)", doc)
if match is not None:
self.name = match.group(1)
if self.name.startswith('*'): # easier than getting the regex to behave...
if self.name.startswith('*'): # Easier than getting the regex to behave...
self.name = self.name.replace('*', '')
self.type = match.group(2)
self.doc = match.group(3)
Expand Down Expand Up @@ -287,7 +311,8 @@ def name_for_call(self):
return '*_'+self.name

def __repr__(self):
return f"Argument('{self.definition}', name='{self.name}', ctype='{self.ctype}', inout_state='{self.inout_state}')"
return (f"Argument('{self.definition}', name='{self.name}', "
f"ctype='{self.ctype}', inout_state='{self.inout_state}')")


class ReturnDoc:
Expand Down Expand Up @@ -454,14 +479,24 @@ def signature(self):

@property
def python_call(self):
outnames = [arg.name for arg in self.args_by_inout('inout|out|stat|ret')]
argnames = [arg.name for arg in self.args_by_inout('in|inout')]
return '{out} = {func}({args})'.format(out=', '.join(outnames),
func='ufunc.' + self.pyname,
args=', '.join(argnames))
out = ', '.join([arg.name for arg in self.args_by_inout('inout|out|stat|ret')])
args = ', '.join([arg.name for arg in self.args_by_inout('in|inout')])
result = '{out} = {func}({args})'.format(out=out,
func='ufunc.' + self.pyname,
args=args)
if len(result) < 75:
return result

if result.index('(') < 75:
return result.replace('(', '(\n ')

split_point = result[:75].rfind(',') + 1
return ('(' + result[:split_point] + '\n '
+ result[split_point:].replace(' =', ') ='))

def __repr__(self):
return f"Function(name='{self.name}', pyname='{self.pyname}', filename='{self.filename}', filepath='{self.filepath}')"
return (f"Function(name='{self.name}', pyname='{self.pyname}', "
f"filename='{self.filename}', filepath='{self.filepath}')")


class Constant:
Expand Down Expand Up @@ -499,20 +534,20 @@ def __init__(self, cname, prototype, pathfordoc):
incomment = False
lastcomment = None
with open(pathfordoc, 'r') as f:
for l in f:
for ln in f:
if incomment:
if l.lstrip().startswith('*/'):
if ln.lstrip().startswith('*/'):
incomment = False
lastcomment = ''.join(lastcomment)
else:
if l.startswith('**'):
l = l[2:]
lastcomment.append(l)
if ln.startswith('**'):
ln = ln[2:]
lastcomment.append(ln)
else:
if l.lstrip().startswith('/*'):
if ln.lstrip().startswith('/*'):
incomment = True
lastcomment = []
if l.startswith(self.prototype):
if ln.startswith(self.prototype):
self.doc = lastcomment
break
else:
Expand Down Expand Up @@ -542,9 +577,10 @@ def main(srcdir=DEFAULT_ERFA_LOC, outfn='core.py', ufuncfn='ufunc.c',
from jinja2 import Environment, FileSystemLoader

if verbose:
print_ = lambda *args, **kwargs: print(*args, **kwargs)
print_ = print
else:
print_ = lambda *args, **kwargs: None
def print_(*args, **kwargs):
return None

# Prepare the jinja2 templating environment
env = Environment(loader=FileSystemLoader(templateloc))
Expand Down