From 42ff7888025c1805ac2f1060acbe2675685c4359 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Wed, 12 Aug 2020 17:08:17 -0400 Subject: [PATCH 1/3] Given python functions more useful docstring titles --- erfa/core.py.templ | 11 ++++------- erfa_generator.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/erfa/core.py.templ b/erfa/core.py.templ index 7ff6c33..618ed9e 100644 --- a/erfa/core.py.templ +++ b/erfa/core.py.templ @@ -131,7 +131,7 @@ dt_bytes12 = numpy.dtype('S12') {% 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 ---------- @@ -147,14 +147,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 }} """ diff --git a/erfa_generator.py b/erfa_generator.py index 49ce0bb..8935261 100644 --- a/erfa_generator.py +++ b/erfa_generator.py @@ -114,6 +114,24 @@ def ret_info(self): 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") From 00c3334bc2c41bc7bbaedd6afdc02dad357d2c7e Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Wed, 12 Aug 2020 19:21:07 -0400 Subject: [PATCH 2/3] PEP8 changes, also such that the generated code is OK. --- erfa/__init__.py | 2 +- erfa/core.py.templ | 78 +++++++++++++++++++++++++---------------- erfa/tests/test_erfa.py | 15 +++++--- erfa_generator.py | 60 ++++++++++++++++++++----------- 4 files changed, 99 insertions(+), 56 deletions(-) diff --git a/erfa/__init__.py b/erfa/__init__.py index cb31bb5..5bffbd8 100644 --- a/erfa/__init__.py +++ b/erfa/__init__.py @@ -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 diff --git a/erfa/core.py.templ b/erfa/core.py.templ index 618ed9e..e892733 100644 --- a/erfa/core.py.templ +++ b/erfa/core.py.templ @@ -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): @@ -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------------------------> @@ -124,11 +134,13 @@ 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(', ') }}): """ {{ func.doc.title }} @@ -180,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!) #} \ No newline at end of file diff --git a/erfa/tests/test_erfa.py b/erfa/tests/test_erfa.py index a64e93d..6e285c0 100644 --- a/erfa/tests/test_erfa.py +++ b/erfa/tests/test_erfa.py @@ -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) @@ -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(): diff --git a/erfa_generator.py b/erfa_generator.py index 8935261..353cf70 100644 --- a/erfa_generator.py +++ b/erfa_generator.py @@ -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: @@ -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))) @@ -110,7 +114,8 @@ 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 @@ -133,7 +138,8 @@ def title(self): 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: @@ -142,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) @@ -305,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: @@ -472,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: @@ -517,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: @@ -560,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)) From 32f239d5926da39ec587e927a8894e4770481710 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Wed, 12 Aug 2020 20:23:34 -0400 Subject: [PATCH 3/3] Remove a few out-of-date astropy._erfa mentions --- erfa/ufunc.c.templ | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erfa/ufunc.c.templ b/erfa/ufunc.c.templ index 6122947..c713f3b 100644 --- a/erfa/ufunc.c.templ +++ b/erfa/ufunc.c.templ @@ -29,7 +29,7 @@ "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" \ @@ -37,7 +37,7 @@ "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" \ @@ -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());