Skip to content

Commit

Permalink
feat: add sticky notes (v3.2.0)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vanderhoof committed Mar 17, 2024
1 parent 048b348 commit f945dc8
Show file tree
Hide file tree
Showing 17 changed files with 303 additions and 19 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022
Copyright (c) 2024

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
1 change: 0 additions & 1 deletion TODO.md

This file was deleted.

13 changes: 13 additions & 0 deletions docs/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [Reference](#reference)
* [Enum](#enum)
* [Note](#note)
* [StickyNote](#sticky_note)
* [Expression](#expression)
* [Project](#project)
* [TableGroup](#tablegroup)
Expand Down Expand Up @@ -260,6 +261,18 @@ Note is a basic class, which may appear in some other classes' `note` attribute.
* **sql** (str) — SQL definition for this note.
* **dbml** (str) — DBML definition for this note.

## Note

**new in PyDBML 1.0.10**

Sticky notes are similar to regular notes, except that they are defined at the root of your DBML file and have a name.

### Attributes

**name** (str) — note name.
**text** (str) — note text.
* **dbml** (str) — DBML definition for this note.

## Expression

**new in PyDBML 1.0.0**
Expand Down
2 changes: 1 addition & 1 deletion pydbml/classes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SQLObject:
Base class for all SQL objects.
'''
required_attributes: Tuple[str, ...] = ()
dont_compare_fields = ()
dont_compare_fields: Tuple[str, ...] = ()

def check_attributes_for_sql(self):
'''
Expand Down
4 changes: 3 additions & 1 deletion pydbml/classes/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ def __init__(self,
self.default = default
self.table: Optional['Table'] = None

def __eq__(self, other: 'Column') -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return False
self_table = self.table.full_name if self.table else None
other_table = other.table.full_name if other.table else None
if self_table != other_table:
Expand Down
7 changes: 2 additions & 5 deletions pydbml/classes/note.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@
class Note(SQLObject):
dont_compare_fields = ('parent',)

def __init__(self, text: Union[str, 'Note']) -> None:
def __init__(self, text: Any) -> None:
self.text: str
if isinstance(text, Note):
self.text = text.text
else:
self.text = str(text) if text else ''
self.text = str(text) if text is not None else ''
self.parent: Any = None

def __str__(self):
Expand Down
50 changes: 50 additions & 0 deletions pydbml/classes/sticky_note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import re
from typing import Any

from pydbml.tools import indent


class StickyNote:
dont_compare_fields = ('database',)

def __init__(self, name: str, text: Any) -> None:
self.name = name
self.text = str(text) if text is not None else ''

self.database = None

def __str__(self):
'''
>>> print(StickyNote('mynote', 'Note text'))
StickyNote('mynote', 'Note text')
'''

return self.__class__.__name__ + f'({repr(self.name)}, {repr(self.text)})'

def __bool__(self):
return bool(self.text)

def __repr__(self):
'''
>>> StickyNote('mynote', 'Note text')
<StickyNote 'mynote', 'Note text'>
'''

return f'<{self.__class__.__name__} {self.name!r}, {self.text!r}>'

def _prepare_text_for_dbml(self):
'''Escape single quotes'''
pattern = re.compile(r"('''|')")
return pattern.sub(r'\\\1', self.text)

@property
def dbml(self):
text = self._prepare_text_for_dbml()
if '\n' in text:
note_text = f"'''\n{text}\n'''"
else:
note_text = f"'{text}'"

note_text = indent(note_text)
result = f'Note {self.name} {{\n{note_text}\n}}'
return result
13 changes: 11 additions & 2 deletions pydbml/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from typing import Optional
from typing import Union

from .classes import Enum
from .classes import Enum, Note
from .classes import Project
from .classes import Reference
from .classes import Table
from .classes import TableGroup
from .classes.sticky_note import StickyNote
from .exceptions import DatabaseValidationError

from .constants import MANY_TO_ONE, ONE_TO_MANY
Expand Down Expand Up @@ -41,6 +42,7 @@ def __init__(self) -> None:
self.refs: List['Reference'] = []
self.enums: List['Enum'] = []
self.table_groups: List['TableGroup'] = []
self.sticky_notes: List['StickyNote'] = []
self.project: Optional['Project'] = None

def __repr__(self) -> str:
Expand Down Expand Up @@ -79,6 +81,8 @@ def add(self, obj: Any) -> Any:
return self.add_table_group(obj)
elif isinstance(obj, Project):
return self.add_project(obj)
elif isinstance(obj, StickyNote):
return self.add_sticky_note(obj)
else:
raise DatabaseValidationError(f'Unsupported type {type(obj)}.')

Expand Down Expand Up @@ -125,6 +129,11 @@ def add_enum(self, obj: Enum) -> Enum:
self.enums.append(obj)
return obj

def add_sticky_note(self, obj: StickyNote) -> StickyNote:
self._set_database(obj)
self.sticky_notes.append(obj)
return obj

def add_table_group(self, obj: TableGroup) -> TableGroup:
if obj in self.table_groups:
raise DatabaseValidationError(f'{obj} is already in the database.')
Expand Down Expand Up @@ -216,7 +225,7 @@ def dbml(self):
'''Generates DBML code out of parsed results'''
items = [self.project] if self.project else []
refs = (ref for ref in self.refs if not ref.inline)
items.extend((*self.enums, *self.tables, *refs, *self.table_groups))
items.extend((*self.enums, *self.tables, *refs, *self.table_groups, *self.sticky_notes))
components = (
i.dbml for i in items
)
Expand Down
21 changes: 21 additions & 0 deletions pydbml/definitions/sticky_note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pyparsing as pp

from .common import _, end, _c
from .generic import string_literal, name
from ..parser.blueprints import StickyNoteBlueprint

sticky_note = _c + pp.CaselessLiteral('note') + _ + (name('name') + _ - '{' + _ - string_literal('text') + _ - '}') + end


def parse_sticky_note(s, loc, tok):
'''
Note single_line_note {
'This is a single line note'
}
'''
init_dict = {'name': tok['name'], 'text': tok['text']}

return StickyNoteBlueprint(**init_dict)


sticky_note.set_parse_action(parse_sticky_note)
18 changes: 18 additions & 0 deletions pydbml/parser/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pydbml.classes import Reference
from pydbml.classes import Table
from pydbml.classes import TableGroup
from pydbml.classes.sticky_note import StickyNote
from pydbml.exceptions import ColumnNotFoundError
from pydbml.exceptions import TableNotFoundError
from pydbml.exceptions import ValidationError
Expand All @@ -43,6 +44,23 @@ def build(self) -> 'Note':
return Note(text)


@dataclass
class StickyNoteBlueprint(Blueprint):
name: str
text: str

def _preformat_text(self) -> str:
'''Preformat the note text for idempotence'''
result = strip_empty_lines(self.text)
result = remove_indentation(result)
return result

def build(self) -> StickyNote:
text = self._preformat_text()
name = self.name
return StickyNote(name=name, text=text)


@dataclass
class ExpressionBlueprint(Blueprint):
text: str
Expand Down
21 changes: 15 additions & 6 deletions pydbml/parser/parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations

from io import TextIOWrapper
from pathlib import Path
from typing import List
Expand All @@ -7,22 +8,22 @@

import pyparsing as pp

from .blueprints import EnumBlueprint
from .blueprints import ProjectBlueprint
from .blueprints import ReferenceBlueprint
from .blueprints import TableBlueprint
from .blueprints import TableGroupBlueprint
from pydbml.classes import Table
from pydbml.database import Database
from pydbml.definitions.common import comment
from pydbml.definitions.enum import enum
from pydbml.definitions.project import project
from pydbml.definitions.reference import ref
from pydbml.definitions.sticky_note import sticky_note
from pydbml.definitions.table import table
from pydbml.definitions.table_group import table_group
from pydbml.exceptions import TableNotFoundError
from pydbml.tools import remove_bom

from .blueprints import EnumBlueprint, StickyNoteBlueprint
from .blueprints import ProjectBlueprint
from .blueprints import ReferenceBlueprint
from .blueprints import TableBlueprint
from .blueprints import TableGroupBlueprint

pp.ParserElement.set_default_whitespace_chars(' \t\r')

Expand Down Expand Up @@ -99,6 +100,7 @@ def __init__(self, source: str):
self.refs: List[ReferenceBlueprint] = []
self.enums: List[EnumBlueprint] = []
self.project: Optional[ProjectBlueprint] = None
self.sticky_notes: List[StickyNoteBlueprint] = []

def parse(self):
self._set_syntax()
Expand All @@ -120,19 +122,22 @@ def _set_syntax(self):
enum_expr = enum.copy()
table_group_expr = table_group.copy()
project_expr = project.copy()
note_expr = sticky_note.copy()

table_expr.addParseAction(self.parse_blueprint)
ref_expr.addParseAction(self.parse_blueprint)
enum_expr.addParseAction(self.parse_blueprint)
table_group_expr.addParseAction(self.parse_blueprint)
project_expr.addParseAction(self.parse_blueprint)
note_expr.addParseAction(self.parse_blueprint)

expr = (
table_expr
| ref_expr
| enum_expr
| table_group_expr
| project_expr
| note_expr
)
self._syntax = expr[...] + ('\n' | comment)[...] + pp.StringEnd()

Expand Down Expand Up @@ -169,6 +174,8 @@ def parse_blueprint(self, s, loc, tok):
self.project = blueprint
if blueprint.note:
blueprint.note.parser = self
elif isinstance(blueprint, StickyNoteBlueprint):
self.sticky_notes.append(blueprint)
else:
raise RuntimeError(f'type unknown: {blueprint}')
blueprint.parser = self
Expand All @@ -194,6 +201,8 @@ def build_database(self):
self.ref_blueprints.extend(table_bp.get_reference_blueprints())
for table_group_bp in self.table_groups:
self.database.add(table_group_bp.build())
for note_bp in self.sticky_notes:
self.database.add(note_bp.build())
if self.project:
self.database.add(self.project.build())
for ref_bp in self.refs:
Expand Down
53 changes: 53 additions & 0 deletions test/test_blueprints/test_sticky_note.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from unittest import TestCase

from pydbml.classes.sticky_note import StickyNote
from pydbml.parser.blueprints import StickyNoteBlueprint

class TestNote(TestCase):
def test_build(self) -> None:
bp = StickyNoteBlueprint(name='mynote', text='Note text')
result = bp.build()
self.assertIsInstance(result, StickyNote)
self.assertEqual(result.name, bp.name)
self.assertEqual(result.text, bp.text)

def test_preformat_not_needed(self):
oneline = 'One line of note text'
multiline = 'Multiline\nnote\n\ntext'
long_line = 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Aspernatur quidem adipisci, impedit, ut illum dolorum consequatur odio voluptate numquam ea itaque excepturi, a libero placeat corrupti. Amet beatae suscipit necessitatibus. Ea expedita explicabo iste quae rem aliquam minus cumque eveniet enim delectus, alias aut impedit quaerat quia ex, aliquid sint amet iusto rerum! Sunt deserunt ea saepe corrupti officiis. Assumenda.'

bp = StickyNoteBlueprint(name='mynote', text=oneline)
self.assertEqual(bp.name, bp.name)
self.assertEqual(bp._preformat_text(), oneline)
bp = StickyNoteBlueprint(name='mynote', text=multiline)
self.assertEqual(bp.name, bp.name)
self.assertEqual(bp._preformat_text(), multiline)
bp = StickyNoteBlueprint(name='mynote', text=long_line)
self.assertEqual(bp.name, bp.name)
self.assertEqual(bp._preformat_text(), long_line)

def test_preformat_needed(self):
uniform_indentation = ' line1\n line2\n line3'
varied_indentation = ' line1\n line2\n\n line3'
empty_lines = '\n\n\n\n\n\n\nline1\nline2\nline3\n\n\n\n\n\n\n'
empty_indented_lines = '\n \n\n \n\n line1\n line2\n line3\n\n\n\n \n\n\n'

exptected = 'line1\nline2\nline3'
bp = StickyNoteBlueprint(name='mynote', text=uniform_indentation)
self.assertEqual(bp._preformat_text(), exptected)
self.assertEqual(bp.name, bp.name)

exptected = 'line1\n line2\n\n line3'
bp = StickyNoteBlueprint(name='mynote', text=varied_indentation)
self.assertEqual(bp._preformat_text(), exptected)
self.assertEqual(bp.name, bp.name)

exptected = 'line1\nline2\nline3'
bp = StickyNoteBlueprint(name='mynote', text=empty_lines)
self.assertEqual(bp._preformat_text(), exptected)
self.assertEqual(bp.name, bp.name)

exptected = 'line1\nline2\nline3'
bp = StickyNoteBlueprint(name='mynote', text=empty_indented_lines)
self.assertEqual(bp._preformat_text(), exptected)
self.assertEqual(bp.name, bp.name)
Loading

0 comments on commit f945dc8

Please sign in to comment.