Skip to content

Commit ca86e1c

Browse files
committed
Added test to do some basic checking of docstrings
1 parent c30a493 commit ca86e1c

File tree

2 files changed

+111
-16
lines changed

2 files changed

+111
-16
lines changed

star_alchemy/_star_alchemy.py

+38-13
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class Join:
3131

3232
@property
3333
def name(self) -> str:
34+
"""
35+
:return: The name of the right hand table in this join.
36+
"""
3437
assert self.table.name is not None
3538
return self.table.name
3639

@@ -63,35 +66,52 @@ class StarSchema:
6366
@property
6467
def tables(self) -> SqlAlchemyTableDict:
6568
"""
66-
:return:
69+
:return: Dictionary mapping names to the SQLAlchemy tables
70+
referenced by this schema.
6771
"""
6872
if self._tables is None:
6973
self._tables = {s.name: s.table for s in self}
7074
return self._tables
7175

7276
def select(self, *args, **kwargs) -> StarSchemaSelect:
7377
"""
78+
:param args: star args to pass to select
79+
:param kwargs: double star args to pass to select
80+
81+
For more information consult the SQLAlchemy documentation:
7482
75-
:return:
83+
https://docs.sqlalchemy.org/en/latest/core/selectable.html#sqlalchemy.sql.expression.select
84+
85+
:return: StarSchemaSelect instance which will auto generate it's
86+
select_from upon compilation.
7687
"""
7788
return StarSchemaSelect(self, *args, **kwargs)
7889

7990
@property
8091
def schemas(self) -> StarSchemaDict:
8192
"""
82-
:return:
93+
:return: Dictionary mapping table names to subschemas.
8394
"""
8495
if self._schemas is None:
8596
self._schemas = {s.name: s for s in self}
8697
return self._schemas
8798

88-
def __getitem__(self, item) -> 'StarSchema':
89-
return self.schemas[item]
99+
def __getitem__(self, name) -> 'StarSchema':
100+
"""
101+
Traverse the tree and retrieve the sub schema with the given
102+
name.
103+
104+
:param name: The name (this is the name of the SQLAlchemy table
105+
the sub schema holds) of the sub schema to get.
106+
107+
:return: Sub schema referenced by the requested name.
108+
"""
109+
return self.schemas[name]
90110

91111
@property
92112
def path(self) -> typing.List['StarSchema']:
93113
"""
94-
:return:
114+
:return: Path to the root of the schema.
95115
"""
96116
def make_path(star_schema) -> typing.Iterator[StarSchema]:
97117
yield star_schema
@@ -100,10 +120,16 @@ def make_path(star_schema) -> typing.Iterator[StarSchema]:
100120

101121
@property
102122
def name(self) -> str:
123+
"""
124+
:return: The name of the table this schema references.
125+
"""
103126
return self.join.name
104127

105128
@property
106129
def table(self) -> SqlAlchemyTable:
130+
"""
131+
:return: The Sqlachemy table referenced by this schema.
132+
"""
107133
return self.join.table
108134

109135
def detach(self, table_name: str) -> 'StarSchema':
@@ -130,8 +156,8 @@ def clone(schema, parent):
130156

131157
def __iter__(self) -> typing.Iterator['StarSchema']:
132158
"""
133-
134-
:return:
159+
:return: Iterator which traverses the tree in a depth first
160+
fashion.
135161
"""
136162
def recurse(star_schema: StarSchema) -> typing.Iterable['StarSchema']:
137163
yield star_schema
@@ -140,6 +166,9 @@ def recurse(star_schema: StarSchema) -> typing.Iterable['StarSchema']:
140166
return iter(recurse(self))
141167

142168
def __hash__(self) -> int:
169+
"""
170+
:return: Hash value to uniquely identify this instance.
171+
"""
143172
return hash(self.join) | hash(self.parent)
144173

145174
@classmethod
@@ -195,14 +224,10 @@ def compile_star_schema_select(element: StarSchemaSelect, compiler, **kw):
195224
# edges in these paths are the joins that need to be made
196225
joins = (
197226
star_schema
198-
199227
for expression in iterate(element, {'column_collections': False})
200228
if isinstance(expression, Column) # only interested in columns
201229
if not isinstance(expression.table, StarSchemaSelect) # but not the select itself
202-
203-
for star_schema in element.star_schema[expression.table.name].path
204-
if star_schema.parent is not None # don't need to create a join from
205-
# root -> root
230+
for star_schema in element.star_schema[expression.table.name].path[1:]
206231
)
207232

208233
# generate the select_from using all the referenced tables

tests/test_star_schema.py

+73-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import inspect
2+
import re
3+
import typing
4+
from contextlib import suppress
15
from unittest import TestCase
26

37
import sqlalchemy as sa
48

59
from examples.sales import tables
610
from star_alchemy import _star_alchemy
7-
from star_alchemy._star_alchemy import Join, StarSchema
11+
from star_alchemy._star_alchemy import Join, StarSchema, StarSchemaSelect
812
from tests import tables
913
from tests.util import AssertQueryEqualMixin, DocTestMixin, query_test
1014

@@ -196,5 +200,71 @@ def test_detach(self):
196200
return product.select([product.tables['category'].c.id])
197201

198202

199-
class DocTestCase(TestCase, DocTestMixin(_star_alchemy)):
200-
pass
203+
class DocStringTestCase(TestCase, DocTestMixin(_star_alchemy)):
204+
"""
205+
Check the docstrings of star_alchemy module, couldn't find a library
206+
that checks docstrings in rst format so fudging my own. The main
207+
things I care about...
208+
209+
* Parameters and return types are correctly documented
210+
* Examples are correct.
211+
212+
Most docstring checking focus on the format, which is less important
213+
for me.
214+
"""
215+
216+
@property
217+
def sub_tests(self):
218+
"""
219+
Iterate through all the functions which should have their
220+
docstring checked for completeness.
221+
"""
222+
def filter_member(o):
223+
return (
224+
(inspect.isfunction(o) or isinstance(o, property))
225+
and (o.__doc__ is None or not o.__doc__.startswith('Method generated by attrs'))
226+
)
227+
228+
# TODO: StarSchemaSelect, compile_star_schema_select
229+
for cls in StarSchema, Join:
230+
for name, member in inspect.getmembers(cls, predicate=filter_member):
231+
if name in ("__delattr__", "__setattr__"):
232+
continue
233+
234+
if isinstance(member, property):
235+
file = inspect.getfile(member.fget)
236+
line = inspect.getsourcelines(member.fget)[1]
237+
else:
238+
file = inspect.getfile(member)
239+
line = inspect.getsourcelines(member)[1]
240+
yield f'{cls.__name__}.{name}', member, f'File "{file}", line {line}'
241+
242+
def test_docstring_should_be_present(self):
243+
for name, member, location in self.sub_tests:
244+
with self.subTest(name):
245+
if member.__doc__ is None:
246+
self.fail(f'Missing docstring ({location})')
247+
248+
def test_docstring_return_should_be_documented(self):
249+
for name, member, location in self.sub_tests:
250+
with self.subTest(name):
251+
if member.__doc__ is None:
252+
self.skipTest(f'Missing docstring ({location})')
253+
if ":return:" not in member.__doc__:
254+
self.fail(f'Missing :return: ({location})')
255+
if re.search(r':return:\s*\n', member.__doc__):
256+
self.fail(f'Empty :return: ({location})')
257+
258+
def test_all_parameters_should_be_documented(self):
259+
for name, member, location in self.sub_tests:
260+
with self.subTest(name):
261+
if not isinstance(member, property):
262+
if member.__doc__ is None:
263+
self.skipTest(f'Missing docstring ({location})')
264+
for parameter in inspect.signature(member).parameters.values():
265+
param = f':param {parameter.name}:'
266+
if parameter.name != 'self':
267+
if param not in member.__doc__:
268+
self.fail(f'Missing {param} ({location})')
269+
if re.search(rf'{param}\s*\n', member.__doc__):
270+
self.fail(f'Empty {param} ({location})')

0 commit comments

Comments
 (0)