data-flow-diagram 1.12.1.post2__tar.gz → 1.13.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. data_flow_diagram-1.13.1/CHANGES.md +102 -0
  2. data_flow_diagram-1.13.1/MANIFEST.in +1 -0
  3. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/PKG-INFO +1 -1
  4. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/__init__.py +55 -55
  5. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/dependency_checker.py +8 -8
  6. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/dfd.py +61 -54
  7. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/dfd_dot_templates.py +2 -2
  8. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/dot.py +7 -7
  9. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/error.py +1 -1
  10. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/markdown.py +10 -10
  11. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/model.py +27 -26
  12. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/parser.py +65 -85
  13. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/scanner.py +13 -18
  14. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram.egg-info/PKG-INFO +1 -1
  15. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram.egg-info/SOURCES.txt +2 -0
  16. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/README.md +0 -0
  17. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/pyproject.toml +0 -0
  18. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/setup.cfg +0 -0
  19. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/setup.py +0 -0
  20. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram/config.py +0 -0
  21. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram.egg-info/dependency_links.txt +0 -0
  22. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram.egg-info/entry_points.txt +0 -0
  23. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram.egg-info/requires.txt +0 -0
  24. {data_flow_diagram-1.12.1.post2 → data_flow_diagram-1.13.1}/src/data_flow_diagram.egg-info/top_level.txt +0 -0
@@ -0,0 +1,102 @@
1
+ ## Version 1.13.1:
2
+
3
+ - Supports style rotated (and unrotated).
4
+
5
+ ## Version 1.12.1.post3:
6
+
7
+ - CHANGES.md is read by setup.py to deduce the version.
8
+
9
+ ## Version 1.12.0:
10
+
11
+ - Support style (and hence attrib) on Stores and Channels.
12
+
13
+ ## Version 1.11.1.post2:
14
+
15
+ Bug fixes:
16
+
17
+ - Apply attribs on frames.
18
+ - Attribs are matched by whole names, so e.g. DATA and DATABASE will work.
19
+
20
+ Improvements:
21
+
22
+ - 'make install' to install locally.
23
+
24
+ ## Version 1.11.0:
25
+
26
+ - Keyword "attrib" to define styles.
27
+
28
+ ## Version 1.10.1:
29
+
30
+ - When item text is numbered, add newline after the number.
31
+
32
+ ## Version 1.9.1:
33
+
34
+ - Add troubleshooting in README.md.
35
+
36
+ ## Version 1.9.0:
37
+
38
+ - Allow line continuation with a trailing backslash
39
+
40
+ ## Version 1.8.0:
41
+
42
+ - Add frames
43
+
44
+ ## Version 1.7.1:
45
+
46
+ - Support continuous back- and relaxed- flows.
47
+
48
+ ## Version 1.7.0:
49
+
50
+ - Add continuous flow (cflow or -->>).
51
+ - Add control (may only connect to signals).
52
+
53
+ ## Version 1.6.0:
54
+
55
+ - Dependencies:
56
+ - items with name #SNIPPET:[NAME] or FILE:[NAME] refer to another graph,
57
+ - referred item is rendered "ghosted",
58
+ - dependencies are checked (unless --no-check-dependencies is passed).
59
+ - Add graph title (unless --no-graph-title is passed).
60
+ - Error in snippets of MD files: display line number relative to MD file (not snippet).
61
+
62
+ ## Version 1.5.0:
63
+
64
+ - Wrap labels by `style item-text-width N` (default N=20).
65
+ - and `style connection-text-width N` (default N=14).
66
+
67
+ ## Version 1.4.1:
68
+
69
+ - Processes have very light grey backgrounds.
70
+ - Add the 'none' item type.
71
+ - Connections with reversed direction affect the items placements.
72
+ - A '?' postfix to a connection, removes the edge constraint.
73
+ - Fix formatting of '\n' for Store and Channel (which are HTML nodes).
74
+ - Colorize error messages.
75
+ - Add drawable attributes as [ATTRS...] prefix before labels.
76
+
77
+ ## Version 1.3.x:
78
+
79
+ - Style vertical: is supported.
80
+ - Style context: for context (top-level) diagrams.
81
+ - Add undirected flow (uflow aka '--').
82
+
83
+ ## Version 1.2.3
84
+
85
+ - Snippet reference and ungenerated snippet were marked with '<'; now
86
+ use '#' instead.
87
+ - Detect include (infinite) recursions and print error.
88
+ - Can print its own version.
89
+
90
+ ## Version 1.1.1
91
+
92
+ - Fix bug with left/bidir arrows.
93
+
94
+ ## Version 1.1.0
95
+
96
+ - Upon error, print error stack trace.
97
+ - Items: label can be ommitted.
98
+ - Connections: syntactic sugars with arrows.
99
+
100
+ ## Version 1.0.0
101
+
102
+ - Initial release.
@@ -0,0 +1 @@
1
+ include CHANGES.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: data-flow-diagram
3
- Version: 1.12.1.post2
3
+ Version: 1.13.1
4
4
  Summary: Commandline tool to generate data flow diagrams from text
5
5
  Home-page: https://github.com/pbauermeister/dfd
6
6
  Author: Pascal Bauermeister
@@ -27,96 +27,96 @@ from .error import print_error
27
27
  try:
28
28
  VERSION = pkg_resources.require("data-flow-diagram")[0].version
29
29
  except pkg_resources.DistributionNotFound:
30
- VERSION = 'undefined'
30
+ VERSION = "undefined"
31
31
 
32
32
 
33
33
  def parse_args() -> argparse.Namespace:
34
- description, epilog = [each.strip() for each in __doc__.split('-----')[:2]]
34
+ description, epilog = [each.strip() for each in __doc__.split("-----")[:2]]
35
35
 
36
36
  parser = argparse.ArgumentParser(description=description, epilog=epilog)
37
37
 
38
38
  parser.add_argument(
39
- 'INPUT_FILE',
40
- action='store',
39
+ "INPUT_FILE",
40
+ action="store",
41
41
  default=None,
42
- nargs='?',
43
- help='UML sequence input file; ' 'if omitted, stdin is used',
42
+ nargs="?",
43
+ help="UML sequence input file; " "if omitted, stdin is used",
44
44
  )
45
45
 
46
46
  parser.add_argument(
47
- '--output-file',
48
- '-o',
47
+ "--output-file",
48
+ "-o",
49
49
  required=False,
50
- help='output file name; pass \'-\' to use stdout; '
51
- 'if omitted, use INPUT_FILE base name with \'.svg\' '
52
- 'extension, or stdout',
50
+ help="output file name; pass '-' to use stdout; "
51
+ "if omitted, use INPUT_FILE base name with '.svg' "
52
+ "extension, or stdout",
53
53
  )
54
54
 
55
55
  parser.add_argument(
56
- '--markdown',
57
- '-m',
58
- action='store_true',
59
- help='consider snippets between opening marker: '
60
- '```data-flow-diagram OUTFILE, and closing marker: ``` '
61
- 'allowing to generate all diagrams contained in an '
62
- 'INPUT_FILE that is a markdown file',
56
+ "--markdown",
57
+ "-m",
58
+ action="store_true",
59
+ help="consider snippets between opening marker: "
60
+ "```data-flow-diagram OUTFILE, and closing marker: ``` "
61
+ "allowing to generate all diagrams contained in an "
62
+ "INPUT_FILE that is a markdown file",
63
63
  )
64
64
 
65
65
  parser.add_argument(
66
- '--format',
67
- '-f',
66
+ "--format",
67
+ "-f",
68
68
  required=False,
69
- default='svg',
70
- help='output format: gif, jpg, tiff, bmp, pnm, eps, '
71
- 'pdf, svg (any supported by Graphviz); default is svg',
69
+ default="svg",
70
+ help="output format: gif, jpg, tiff, bmp, pnm, eps, "
71
+ "pdf, svg (any supported by Graphviz); default is svg",
72
72
  )
73
73
 
74
74
  parser.add_argument(
75
- '--percent-zoom',
76
- '-p',
75
+ "--percent-zoom",
76
+ "-p",
77
77
  required=False,
78
78
  default=100,
79
79
  type=int,
80
- help='magnification percentage; default is 100',
80
+ help="magnification percentage; default is 100",
81
81
  )
82
82
 
83
83
  parser.add_argument(
84
- '--background-color',
85
- '-b',
84
+ "--background-color",
85
+ "-b",
86
86
  required=False,
87
- default='white',
88
- help='background color name (including \'none\' for'
89
- ' transparent) in web color notation; see'
90
- ' https://developer.mozilla.org/en-US/docs/Web/CSS/color_value'
91
- ' for a list of valid names; default is white',
87
+ default="white",
88
+ help="background color name (including 'none' for"
89
+ " transparent) in web color notation; see"
90
+ " https://developer.mozilla.org/en-US/docs/Web/CSS/color_value"
91
+ " for a list of valid names; default is white",
92
92
  )
93
93
 
94
94
  parser.add_argument(
95
- '--no-graph-title',
96
- action='store_true',
95
+ "--no-graph-title",
96
+ action="store_true",
97
97
  default=False,
98
- help='suppress graph title',
98
+ help="suppress graph title",
99
99
  )
100
100
 
101
101
  parser.add_argument(
102
- '--no-check-dependencies',
103
- action='store_true',
102
+ "--no-check-dependencies",
103
+ action="store_true",
104
104
  default=False,
105
- help='suppress dependencies checking',
105
+ help="suppress dependencies checking",
106
106
  )
107
107
 
108
108
  parser.add_argument(
109
- '--debug',
110
- action='store_true',
109
+ "--debug",
110
+ action="store_true",
111
111
  default=False,
112
- help='emit debug messages',
112
+ help="emit debug messages",
113
113
  )
114
114
 
115
115
  parser.add_argument(
116
- '--version',
117
- '-V',
118
- action='store_true',
119
- help='print the version and exit',
116
+ "--version",
117
+ "-V",
118
+ action="store_true",
119
+ help="print the version and exit",
120
120
  )
121
121
 
122
122
  return parser.parse_args()
@@ -137,17 +137,17 @@ def handle_markdown_source(
137
137
  options,
138
138
  snippet_by_name=params.snippet_by_name,
139
139
  )
140
- print(f'{sys.argv[0]}: generated {params.file_name}', file=sys.stderr)
140
+ print(f"{sys.argv[0]}: generated {params.file_name}", file=sys.stderr)
141
141
 
142
142
 
143
143
  def handle_dfd_source(
144
144
  options: model.Options, provenance: str, input_fp: TextIO, output_path: str
145
145
  ) -> None:
146
146
  root = model.SourceLine("", provenance, None, None)
147
- if output_path == '-':
147
+ if output_path == "-":
148
148
  # output to stdout
149
149
  with tempfile.TemporaryDirectory() as d:
150
- path = os.path.join(d, 'file.svg')
150
+ path = os.path.join(d, "file.svg")
151
151
  dfd.build(root, input_fp.read(), path, options)
152
152
  with open(path) as f:
153
153
  print(f.read())
@@ -160,10 +160,10 @@ def run(args: argparse.Namespace) -> None:
160
160
  # adjust input
161
161
  if args.INPUT_FILE is None:
162
162
  input_fp = sys.stdin
163
- provenance = '<stdin>'
163
+ provenance = "<stdin>"
164
164
  else:
165
165
  input_fp = open(args.INPUT_FILE)
166
- provenance = f'<file:{args.INPUT_FILE}>'
166
+ provenance = f"<file:{args.INPUT_FILE}>"
167
167
 
168
168
  options = model.Options(
169
169
  args.format,
@@ -183,9 +183,9 @@ def run(args: argparse.Namespace) -> None:
183
183
  if args.output_file is None:
184
184
  if args.INPUT_FILE is not None:
185
185
  basename = os.path.splitext(args.INPUT_FILE)[0]
186
- output_path = basename + '.' + args.format
186
+ output_path = basename + "." + args.format
187
187
  else:
188
- output_path = '-'
188
+ output_path = "-"
189
189
  else:
190
190
  output_path = args.output_file
191
191
 
@@ -200,12 +200,12 @@ def main() -> None:
200
200
 
201
201
  args = parse_args()
202
202
  if args.version:
203
- print('data-flow-diagram', VERSION)
203
+ print("data-flow-diagram", VERSION)
204
204
  sys.exit(0)
205
205
 
206
206
  try:
207
207
  run(args)
208
208
  except model.DfdException as e:
209
- text = f'ERROR: {e}'
209
+ text = f"ERROR: {e}"
210
210
  print_error(text)
211
211
  sys.exit(1)
@@ -15,33 +15,33 @@ def check(
15
15
  prefix = model.mk_err_prefix_from(dep.source)
16
16
 
17
17
  # load source text
18
- if dep.to_graph.startswith('#'):
18
+ if dep.to_graph.startswith("#"):
19
19
  # from snippet
20
20
  name = dep.to_graph[1:]
21
21
  if name not in snippet_by_name:
22
22
  errors.append(f'{prefix}Referring to unknown snippet "{name}"')
23
23
  continue
24
24
  text = snippet_by_name[name].text
25
- what = 'snippet'
25
+ what = "snippet"
26
26
  else:
27
27
  # from file
28
28
  name = dep.to_graph
29
29
  try:
30
- with open(name, encoding='utf-8') as f:
30
+ with open(name, encoding="utf-8") as f:
31
31
  text = f.read()
32
32
  except FileNotFoundError as e:
33
33
  if name in snippet_by_name:
34
34
  errors.append(f'{prefix}{e}. Did you mean "#{name}" ?')
35
35
  else:
36
- errors.append(f'{prefix}{e}')
36
+ errors.append(f"{prefix}{e}")
37
37
  continue
38
- what = 'file'
38
+ what = "file"
39
39
 
40
40
  # if only graph is targetted, we're done
41
41
  if dep.to_item is None:
42
42
  if dep.to_type != model.NONE:
43
43
  errors.append(
44
- f'{prefix}A whole graph may only be referred to '
44
+ f"{prefix}A whole graph may only be referred to "
45
45
  f'by an item of type "{model.NONE}", and not '
46
46
  f'"{dep.to_type}"'
47
47
  )
@@ -69,8 +69,8 @@ def check(
69
69
  )
70
70
 
71
71
  if errors:
72
- errors.insert(0, 'Dependency error(s) found:')
73
- raise model.DfdException('\n\n'.join(errors))
72
+ errors.insert(0, "Dependency error(s) found:")
73
+ raise model.DfdException("\n\n".join(errors))
74
74
 
75
75
 
76
76
  def find_item(name: str, statements: model.Statements) -> model.Item | None:
@@ -41,13 +41,13 @@ def build(
41
41
 
42
42
  def wrap(text: str, cols: int) -> str:
43
43
  res: list[str] = []
44
- for each in text.strip().split('\\n'):
45
- res += textwrap.wrap(each, width=cols, break_long_words=False) or ['']
46
- return '\\n'.join(res)
44
+ for each in text.strip().split("\\n"):
45
+ res += textwrap.wrap(each, width=cols, break_long_words=False) or [""]
46
+ return "\\n".join(res)
47
47
 
48
48
 
49
49
  class Generator:
50
- RX_NUMBERED_NAME = re.compile(r'(\d+[.])(.*)')
50
+ RX_NUMBERED_NAME = re.compile(r"(\d+[.])(.*)")
51
51
 
52
52
  def __init__(
53
53
  self, graph_options: model.GraphOptions, attribs: model.Attribs
@@ -60,32 +60,32 @@ class Generator:
60
60
  self.attribs_rx = self._compile_attribs_names(attribs)
61
61
 
62
62
  def append(self, line: str, statement: model.Statement) -> None:
63
- self.lines.append('')
63
+ self.lines.append("")
64
64
  text = model.pack(statement.source.text)
65
- self.lines.append(f'/* {statement.source.line_nr}: {text} */')
65
+ self.lines.append(f"/* {statement.source.line_nr}: {text} */")
66
66
  self.lines.append(line)
67
67
 
68
68
  def generate_item(self, item: model.Item) -> None:
69
69
  copy = model.Item(**item.__dict__)
70
70
  hits = self.RX_NUMBERED_NAME.findall(copy.text)
71
71
  if hits:
72
- copy.text = '\\n'.join(hits[0])
72
+ copy.text = "\\n".join(hits[0])
73
73
 
74
74
  copy.text = wrap(copy.text, self.graph_options.item_text_width)
75
- attrs = copy.attrs or ''
75
+ attrs = copy.attrs or ""
76
76
  attrs = self._expand_attribs(attrs)
77
77
 
78
78
  match copy.type:
79
79
  case model.PROCESS:
80
80
  if self.graph_options.is_context:
81
- shape = 'circle'
82
- fc = 'white'
81
+ shape = "circle"
82
+ fc = "white"
83
83
  else:
84
- shape = 'ellipse'
84
+ shape = "ellipse"
85
85
  fc = '"#eeeeee"'
86
86
  line = (
87
87
  f'"{copy.name}" [shape={shape} label="{copy.text}" '
88
- f'fillcolor={fc} style=filled {attrs}]'
88
+ f"fillcolor={fc} style=filled {attrs}]"
89
89
  )
90
90
  case model.CONTROL:
91
91
  fc = '"#eeeeee"'
@@ -96,7 +96,7 @@ class Generator:
96
96
  case model.ENTITY:
97
97
  line = (
98
98
  f'"{copy.name}" [shape=rectangle label="{copy.text}" '
99
- f'{attrs}]'
99
+ f"{attrs}]"
100
100
  )
101
101
  case model.STORE:
102
102
  d = self._attrib_to_dict(copy, attrs)
@@ -112,22 +112,22 @@ class Generator:
112
112
  case _:
113
113
  prefix = model.mk_err_prefix_from(copy.source)
114
114
  raise model.DfdException(
115
- f'{prefix}Unsupported item type ' f'"{copy.type}"'
115
+ f"{prefix}Unsupported item type " f'"{copy.type}"'
116
116
  )
117
117
  self.append(line, item)
118
118
 
119
119
  def _attrib_to_dict(self, item: model.Item, attrs: str) -> dict[str, str]:
120
120
  d = self._item_to_html_dict(item)
121
- d.update({'fontcolor': 'black', 'color': 'black'})
121
+ d.update({"fontcolor": "black", "color": "black"})
122
122
  attrs_d = {
123
- k: v for k, v in [each.split('=', 1) for each in attrs.split()]
123
+ k: v for k, v in [each.split("=", 1) for each in attrs.split()]
124
124
  }
125
125
  d.update(attrs_d)
126
126
  return d
127
127
 
128
128
  def _item_to_html_dict(self, item: model.Item) -> dict[str, Any]:
129
129
  d = item.__dict__
130
- d['text'] = d['text'].replace('\\n', '<br/>')
130
+ d["text"] = d["text"].replace("\\n", "<br/>")
131
131
  return d
132
132
 
133
133
  def _compile_attribs_names(
@@ -135,8 +135,8 @@ class Generator:
135
135
  ) -> re.Pattern[str] | None:
136
136
  if not attribs:
137
137
  return None
138
- names = ['\\b' + re.escape(k) + '\\b' for k in attribs.keys()]
139
- pattern = '|'.join(names)
138
+ names = ["\\b" + re.escape(k) + "\\b" for k in attribs.keys()]
139
+ pattern = "|".join(names)
140
140
  return re.compile(pattern)
141
141
 
142
142
  def _expand_attribs(self, attrs: str) -> str:
@@ -144,10 +144,10 @@ class Generator:
144
144
  alias = m[0]
145
145
  if alias not in self.attribs:
146
146
  raise model.DfdException(
147
- f'Alias '
147
+ f"Alias "
148
148
  f'"{alias}" '
149
- f'not found in '
150
- f'{pprint.pformat(self.attribs)}'
149
+ f"not found in "
150
+ f"{pprint.pformat(self.attribs)}"
151
151
  )
152
152
 
153
153
  return self.attribs[alias].text
@@ -158,7 +158,7 @@ class Generator:
158
158
 
159
159
  def generate_star(self, text: str) -> str:
160
160
  text = wrap(text, self.graph_options.item_text_width)
161
- star_name = f'__star_{self.star_nr}__'
161
+ star_name = f"__star_{self.star_nr}__"
162
162
  line = f'"{star_name}" [shape=none label="{text}" {TMPL.DOT_FONT_EDGE}]'
163
163
  self.lines.append(line)
164
164
  self.star_nr += 1
@@ -170,57 +170,57 @@ class Generator:
170
170
  src_item: model.Item | None,
171
171
  dst_item: model.Item | None,
172
172
  ) -> None:
173
- text = conn.text or ''
173
+ text = conn.text or ""
174
174
  text = wrap(text, self.graph_options.connection_text_width)
175
175
 
176
176
  src_port = dst_port = ""
177
177
 
178
178
  if not src_item:
179
179
  src_name = self.generate_star(text)
180
- text = ''
180
+ text = ""
181
181
  else:
182
182
  src_name = src_item.name
183
183
  if src_item.type == model.CHANNEL:
184
- src_port = ':x:c'
184
+ src_port = ":x:c"
185
185
 
186
186
  if not dst_item:
187
187
  dst_name = self.generate_star(text)
188
- text = ''
188
+ text = ""
189
189
  else:
190
190
  dst_name = dst_item.name
191
191
  if dst_item.type == model.CHANNEL:
192
- dst_port = ':x:c'
192
+ dst_port = ":x:c"
193
193
 
194
194
  attrs = f'label="{text}"'
195
195
 
196
196
  if conn.attrs:
197
- attrs += ' ' + self._expand_attribs(conn.attrs)
197
+ attrs += " " + self._expand_attribs(conn.attrs)
198
198
 
199
199
  match conn.type:
200
200
  case model.FLOW:
201
201
  if conn.reversed:
202
- attrs += ' dir=back'
202
+ attrs += " dir=back"
203
203
  case model.BFLOW:
204
- attrs += ' dir=both'
204
+ attrs += " dir=both"
205
205
  case model.CFLOW:
206
206
  if conn.reversed:
207
- attrs += ' dir=back'
208
- attrs += ' arrowtail=normalnormal'
207
+ attrs += " dir=back"
208
+ attrs += " arrowtail=normalnormal"
209
209
  else:
210
- attrs += ' arrowhead=normalnormal'
210
+ attrs += " arrowhead=normalnormal"
211
211
  case model.UFLOW:
212
- attrs += ' dir=none'
212
+ attrs += " dir=none"
213
213
  case model.SIGNAL:
214
214
  if conn.reversed:
215
- attrs += ' dir=back'
216
- attrs += ' style=dashed'
215
+ attrs += " dir=back"
216
+ attrs += " style=dashed"
217
217
  case _:
218
218
  prefix = model.mk_err_prefix_from(conn.source)
219
219
  raise model.DfdException(
220
- f'{prefix}Unsupported connection type ' f'"{conn.type}"'
220
+ f"{prefix}Unsupported connection type " f'"{conn.type}"'
221
221
  )
222
222
  if conn.relaxed:
223
- attrs += ' constraint=false'
223
+ attrs += " constraint=false"
224
224
 
225
225
  line = f'"{src_name}"{src_port} -> "{dst_name}"{dst_port} [{attrs}]'
226
226
  self.append(line, conn)
@@ -229,17 +229,17 @@ class Generator:
229
229
  pass
230
230
 
231
231
  def generate_frame(self, frame: model.Frame) -> None:
232
- self.append(f'subgraph cluster_{self.frame_nr} {{', frame)
232
+ self.append(f"subgraph cluster_{self.frame_nr} {{", frame)
233
233
  self.frame_nr += 1
234
234
 
235
235
  self.lines.append(f' label="{frame.text}"')
236
236
  if frame.attrs:
237
237
  attrs = self._expand_attribs(frame.attrs)
238
- self.lines.append(f' {attrs}')
238
+ self.lines.append(f" {attrs}")
239
239
 
240
240
  for item in frame.items:
241
241
  self.lines.append(f' "{item}"')
242
- self.lines.append('}')
242
+ self.lines.append("}")
243
243
 
244
244
  def generate_dot_text(self, title: str) -> str:
245
245
  graph_params = []
@@ -250,16 +250,19 @@ class Generator:
250
250
  graph_params.append(TMPL.DOT_GRAPH_TITLE.format(title=title))
251
251
 
252
252
  if self.graph_options.is_vertical:
253
- graph_params.append('rankdir=TB')
253
+ graph_params.append("rankdir=TB")
254
254
  else:
255
- graph_params.append('rankdir=LR')
255
+ graph_params.append("rankdir=LR")
256
256
 
257
- block = '\n'.join(self.lines).replace('\n', '\n ')
257
+ if self.graph_options.is_rotated:
258
+ graph_params.append(f"rotate=90")
259
+
260
+ block = "\n".join(self.lines).replace("\n", "\n ")
258
261
  text = TMPL.DOT.format(
259
262
  title=title,
260
263
  block=block,
261
- graph_params='\n '.join(graph_params),
262
- ).replace('\n \n', '\n\n')
264
+ graph_params="\n ".join(graph_params),
265
+ ).replace("\n \n", "\n\n")
263
266
  # print(text)
264
267
  return text
265
268
 
@@ -273,7 +276,7 @@ def generate_dot(
273
276
  """Iterate over statements and generate a dot source file"""
274
277
 
275
278
  def get_item(name: str) -> Optional[model.Item]:
276
- return None if name == '*' else items_by_name[name]
279
+ return None if name == "*" else items_by_name[name]
277
280
 
278
281
  for statement in statements:
279
282
  match statement:
@@ -328,18 +331,22 @@ def handle_options(
328
331
  match statement:
329
332
  case model.Style() as style:
330
333
  match style.style:
331
- case 'vertical':
334
+ case "vertical":
332
335
  options.is_vertical = True
333
- case 'context':
336
+ case "context":
334
337
  options.is_context = True
335
- case 'horizontal':
338
+ case "horizontal":
336
339
  options.is_vertical = False
337
- case 'item-text-width':
340
+ case "rotated":
341
+ options.is_rotated = True
342
+ case "unrotated":
343
+ options.is_rotated = False
344
+ case "item-text-width":
338
345
  try:
339
346
  options.item_text_width = int(style.value)
340
347
  except ValueError as e:
341
348
  raise model.DfdException(f'{prefix}{e}"')
342
- case 'connection-text-width':
349
+ case "connection-text-width":
343
350
  try:
344
351
  options.connection_text_width = int(style.value)
345
352
  except ValueError as e:
@@ -347,7 +354,7 @@ def handle_options(
347
354
 
348
355
  case _:
349
356
  raise model.DfdException(
350
- f'{prefix}Unsupported style ' f'"{style.style}"'
357
+ f"{prefix}Unsupported style " f'"{style.style}"'
351
358
  )
352
359
 
353
360
  continue