Skip to content

Commit ff504dd

Browse files
committed
100% Test Success (#5)
1 parent 7ba481f commit ff504dd

File tree

11 files changed

+198
-39
lines changed

11 files changed

+198
-39
lines changed

cdd/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from logging import getLogger as get_logger
1010

1111
__author__ = "Samuel Marks" # type: str
12-
__version__ = "0.0.99rc46" # type: str
12+
__version__ = "0.0.99rc47" # type: str
1313
__description__ = (
1414
"Open API to/fro routes, models, and tests. "
1515
"Convert between docstrings, classes, methods, argparse, pydantic, and SQLalchemy."

cdd/docstring/utils/parse_utils.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from cdd.shared.ast_utils import deduplicate
1414
from cdd.shared.pure_utils import (
1515
count_iter_items,
16+
pp,
1617
simple_types,
1718
sliding_window,
1819
type_to_name,
@@ -56,6 +57,7 @@
5657
("List", " ", "of"): "List",
5758
("Tuple", " ", "of"): "Tuple",
5859
("Dictionary", " ", "of"): "Mapping",
60+
("One", " ", "of"): "Union",
5961
}
6062

6163

@@ -369,8 +371,19 @@ def _parse_adhoc_doc_for_typ_phase0(doc, words):
369371
word_chars: str = "{0}{1}`'\"/|".format(string.digits, string.ascii_letters)
370372
sentence_ends: int = -1
371373
break_the_union: bool = False # lincoln
372-
for i, ch in enumerate(doc):
373-
if (
374+
counter = Counter(doc) # Imperfect because won't catch escaped quote marks
375+
balanced_single: bool = counter["'"] > 0 and counter["'"] & 1 == 0
376+
balanced_double: bool = counter['"'] > 0 and counter['"'] & 1 == 0
377+
378+
i: int = 0
379+
n: int = len(doc)
380+
while i < n:
381+
ch: str = doc[i]
382+
if (ch == "'" and balanced_single or ch == '"' and balanced_double) and (
383+
i == 0 or doc[i - 1] != "\\"
384+
):
385+
i = eat_quoted(ch, doc, i, words, n)
386+
elif (
374387
ch in word_chars
375388
or ch == "."
376389
and len(doc) > (i + 1)
@@ -380,14 +393,20 @@ def _parse_adhoc_doc_for_typ_phase0(doc, words):
380393
):
381394
words[-1].append(ch)
382395
elif ch in frozenset((".", ";", ",")) or ch.isspace():
383-
words[-1] = "".join(words[-1])
384-
words.append(ch)
396+
if words[-1]:
397+
words[-1] = "".join(words[-1])
398+
words.append(ch)
399+
else:
400+
words[-1] = ch
385401
if ch == "." and sentence_ends == -1:
386402
sentence_ends: int = len(words)
387403
elif ch == ";":
388404
break_the_union = True
389405
words.append([])
406+
i += 1
390407
words[-1] = "".join(words[-1])
408+
if not words[-1]:
409+
del words[-1]
391410
candidate_type: Optional[str] = next(
392411
map(
393412
adhoc_type_to_type.__getitem__,
@@ -414,4 +433,47 @@ def _parse_adhoc_doc_for_typ_phase0(doc, words):
414433
return candidate_type, fst_sentence, sentence
415434

416435

436+
def eat_quoted(ch, doc, chomp_start_idx, words, n):
437+
"""
438+
Chomp from quoted character `ch` to quoted character `ch`
439+
440+
:param ch: Character of `'` or `"`
441+
:type ch: ```Literal["'", '"']```
442+
443+
:param doc: Possibly ambiguous docstring for argument, that *might* hint as to the type
444+
:type doc: ```str```
445+
446+
:param chomp_start_idx: chomp_start_idx
447+
:type chomp_start_idx: ```int```
448+
449+
:param words: Words
450+
:type words: ```List[Union[List[str], str]]```
451+
452+
:param n: Length of `doc`
453+
:type n: ```int```
454+
455+
:return: chomp_end_idx
456+
:rtype: ```int```
457+
"""
458+
chomp_end_idx: int = next(
459+
filter(
460+
lambda _chomp_end_idx: doc[_chomp_end_idx + 1] != ch
461+
or doc[_chomp_end_idx] == "\\",
462+
range(chomp_start_idx, n),
463+
),
464+
chomp_start_idx,
465+
)
466+
quoted_str: str = doc[chomp_start_idx:chomp_end_idx]
467+
from operator import iadd
468+
469+
pp({"b4::words": words, '"".join(words[-1])': "".join(words[-1])})
470+
(
471+
iadd(words, (quoted_str, []))
472+
if len(words[-1]) == 1 and words[-1][-1] == "`"
473+
else iadd(words, ("".join(words[-1]), quoted_str, []))
474+
)
475+
pp({"words": words})
476+
return chomp_end_idx
477+
478+
417479
__all__ = ["parse_adhoc_doc_for_typ"] # type: list[str]

cdd/shared/ast_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,10 @@ def get_default_val(val):
427427
)
428428
elif _param.get("typ") == "Str":
429429
_param["typ"] = "str"
430-
elif _param.get("typ") in frozenset(("Constant", "NameConstant", "Num")):
430+
elif _param.get("typ") in frozenset(("Constant", "NameConstant")):
431431
_param["typ"] = "object"
432+
elif _param.get("typ") == "Num":
433+
_param["typ"] = "float"
432434
if "typ" in _param and needs_quoting(_param["typ"]):
433435
default = (
434436
_param.get("default")

cdd/shared/defaults_utils.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -228,27 +228,38 @@ def _parse_out_default_and_doc(
228228
:rtype: Tuple[str, Optional[str]]
229229
"""
230230
if typ is not None and typ in simple_types and default not in none_types:
231-
lit = (
232-
ast.AST()
233-
if typ != "str"
234-
and any(
235-
map(
236-
partial(contains, frozenset(("*", "^", "&", "|", "$", "@", "!"))),
237-
default,
231+
keep_default: bool = False
232+
try:
233+
lit = (
234+
ast.AST()
235+
if typ != "str"
236+
and any(
237+
map(
238+
partial(
239+
contains, frozenset(("*", "^", "&", "|", "$", "@", "!"))
240+
),
241+
default,
242+
)
238243
)
244+
else literal_eval("({default})".format(default=default))
239245
)
240-
else literal_eval("({default})".format(default=default))
241-
)
246+
except ValueError as e:
247+
assert e.args[0].startswith("malformed node or string"), e
248+
keep_default = True
242249
default = (
243-
"```{default}```".format(default=default)
244-
if isinstance(lit, ast.AST)
245-
else {
246-
"bool": bool,
247-
"int": int,
248-
"float": float,
249-
"complex": complex,
250-
"str": str,
251-
}[typ](lit)
250+
default
251+
if keep_default
252+
else (
253+
"```{default}```".format(default=default)
254+
if isinstance(lit, ast.AST)
255+
else {
256+
"bool": bool,
257+
"int": int,
258+
"float": float,
259+
"complex": complex,
260+
"str": str,
261+
}[typ](lit)
262+
)
252263
)
253264
elif default.isdecimal():
254265
default = int(default)

cdd/sqlalchemy/utils/emit_utils.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
from json import dumps
3939
from operator import attrgetter, eq, methodcaller
4040
from os import path
41-
from platform import system
4241
from typing import Any, Dict, List, Optional
4342

4443
import cdd.shared.ast_utils
@@ -79,8 +78,7 @@ def param_to_sqlalchemy_column_calls(name_param, include_name):
7978
:return: Iterable of elements in form of: `Column(…)`
8079
:rtype: ```Iterable[Call]```
8180
"""
82-
if system() == "Darwin":
83-
print("param_to_sqlalchemy_column_calls::include_name:", include_name, ";")
81+
# if system() == "Darwin": print("param_to_sqlalchemy_column_calls::include_name:", include_name, ";")
8482
name, _param = name_param
8583
del name_param
8684

cdd/sqlalchemy/utils/shared_utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,12 @@ def update_args_infer_typ_sqlalchemy(_param, args, name, nullable, x_typ_sql):
8787
parsed_typ: Call = cast(
8888
Call, cdd.shared.ast_utils.get_value(ast.parse(_param["typ"]).body[0])
8989
)
90-
assert parsed_typ.value.id == "Literal", "Expected `Literal` got: {!r}".format(
91-
parsed_typ.value.id
92-
)
90+
try:
91+
assert (
92+
parsed_typ.value.id == "Literal"
93+
), "Expected `Literal` got: {!r}".format(parsed_typ.value.id)
94+
except AssertionError:
95+
raise
9396
val = cdd.shared.ast_utils.get_value(parsed_typ.slice)
9497
(
9598
args.append(

cdd/tests/mocks/docstrings.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -500,20 +500,22 @@
500500
)
501501
docstring_google_pytorch_lbfgs_str: str = "\n".join(docstring_google_pytorch_lbfgs)
502502

503-
docstring_google_str: str = (
504-
"""{docstring_header_str}
505-
Args:
503+
docstring_google_args_str: str = """Args:
506504
dataset_name (str): name of dataset. Defaults to "mnist"
507505
tfds_dir (str): directory to look for models in. Defaults to "~/tensorflow_datasets"
508506
K (Literal['np', 'tf']): backend engine, e.g., `np` or `tf`. Defaults to "np"
509507
as_numpy (Optional[bool]): Convert to numpy ndarrays
510508
data_loader_kwargs (Optional[dict]): pass this as arguments to data_loader function
511-
512-
Returns:
509+
"""
510+
docstring_google_footer_return_str: str = """Returns:
513511
Union[Tuple[tf.data.Dataset, tf.data.Dataset], Tuple[np.ndarray, np.ndarray]]:
514512
Train and tests dataset splits. Defaults to (np.empty(0), np.empty(0))
515-
""".format(
516-
docstring_header_str=docstring_header_str
513+
"""
514+
docstring_google_str: str = "\n".join(
515+
(
516+
docstring_header_str,
517+
docstring_google_args_str,
518+
docstring_google_footer_return_str,
517519
)
518520
)
519521

cdd/tests/test_compound/test_exmod_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def test_emit_file_on_hierarchy(self) -> None:
5757
with patch(
5858
"cdd.compound.exmod_utils.EXMOD_OUT_STREAM", new_callable=StringIO
5959
), TemporaryDirectory() as tempdir:
60+
output_directory: str = path.join(tempdir, "haz")
6061
open(path.join(tempdir, INIT_FILENAME), "a").close()
6162
emit_file_on_hierarchy(
6263
("foo.bar", "foo_dir", ir),
@@ -65,13 +66,13 @@ def test_emit_file_on_hierarchy(self) -> None:
6566
"",
6667
True,
6768
filesystem_layout="as_input",
68-
output_directory=tempdir,
69+
output_directory=output_directory,
6970
first_output_directory=tempdir,
7071
no_word_wrap=None,
7172
dry_run=False,
7273
extra_modules_to_all=None,
7374
)
74-
self.assertTrue(path.isdir(tempdir))
75+
self.assertTrue(path.isdir(output_directory))
7576

7677
def test__emit_symbols_isfile_emit_filename_true(self) -> None:
7778
"""Test `_emit_symbol` when `isfile_emit_filename is True`"""

cdd/tests/test_marshall_docstring.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,25 @@ def test_from_docstring_google_str(self) -> None:
248248
_intermediate_repr_no_default_doc["doc"] = docstring_header_str
249249
self.assertDictEqual(ir, _intermediate_repr_no_default_doc)
250250

251+
def test_from_docstring_google_str_extra_after(self) -> None:
252+
"""
253+
Tests whether `parse_docstring` produces `intermediate_repr_no_default_doc` + "\n\n\ntrailer"
254+
from `docstring_google_str` + "\n\n\ntrailer"
255+
"""
256+
ir: IntermediateRepr = parse_docstring(
257+
"{}\n\n\ntrailer".format(docstring_google_str)
258+
)
259+
_intermediate_repr_no_default_doc = deepcopy(
260+
intermediate_repr_no_default_with_nones_doc
261+
)
262+
_intermediate_repr_no_default_doc["doc"] = "{0}\n\n\n\ntrailer".format(
263+
docstring_header_str
264+
)
265+
266+
self.assertDictEqual(ir, _intermediate_repr_no_default_doc)
267+
268+
maxDiff = None
269+
251270
def test_from_docstring_google_keras_squared_hinge(self) -> None:
252271
"""
253272
Tests whether `parse_docstring` produces the right IR

cdd/tests/test_shared/test_ast_utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,39 @@ def test_param2ast_with_wrapped_default(self) -> None:
10111011
),
10121012
)
10131013

1014+
def test_param2ast_with_simple_types(self) -> None:
1015+
"""Check that `param2ast` behaves correctly with simple types"""
1016+
deque(
1017+
map(
1018+
lambda typ_res: run_ast_test(
1019+
self,
1020+
param2ast(
1021+
("zion", {"typ": typ_res[0], "default": NoneStr}),
1022+
),
1023+
gold=AnnAssign(
1024+
annotation=Name(
1025+
typ_res[1], Load(), lineno=None, col_offset=None
1026+
),
1027+
simple=1,
1028+
target=Name("zion", Store(), lineno=None, col_offset=None),
1029+
value=set_value(None),
1030+
expr=None,
1031+
expr_target=None,
1032+
expr_annotation=None,
1033+
col_offset=None,
1034+
lineno=None,
1035+
),
1036+
),
1037+
(
1038+
("Str", "str"),
1039+
("Constant", "object"),
1040+
("NameConstant", "object"),
1041+
("Num", "float"),
1042+
),
1043+
),
1044+
maxlen=0,
1045+
)
1046+
10141047
def test_param2argparse_param_none_default(self) -> None:
10151048
"""
10161049
Tests that param2argparse_param works to reparse the default

cdd/tests/test_sqlalchemy/test_emit_sqlalchemy_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,34 @@ def test_update_args_infer_typ_sqlalchemy_early_exit(self) -> None:
337337
(True, None),
338338
)
339339

340+
def test_update_args_infer_typ_sqlalchemy_list_struct(self) -> None:
341+
"""Tests that `update_args_infer_typ_sqlalchemy` behaves correctly on List[struct]"""
342+
args = []
343+
self.assertTupleEqual(
344+
update_args_infer_typ_sqlalchemy(
345+
{"typ": "List[struct]"},
346+
args=args,
347+
name="",
348+
nullable=True,
349+
x_typ_sql={},
350+
),
351+
(True, None),
352+
)
353+
self.assertEqual(len(args), 1)
354+
run_ast_test(
355+
self,
356+
args[0],
357+
Call(
358+
func=Name("ARRAY", Load(), lineno=None, col_offset=None),
359+
args=[Name("JSON", Load(), lineno=None, col_offset=None)],
360+
keywords=[],
361+
expr=None,
362+
expr_func=None,
363+
lineno=None,
364+
col_offset=None,
365+
),
366+
)
367+
340368
def test_update_with_imports_from_columns(self) -> None:
341369
"""
342370
Tests basic `cdd.sqlalchemy.utils.emit_utils.update_with_imports_from_columns` usage

0 commit comments

Comments
 (0)