formol 0.1.0__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.
formol-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.1
2
+ Name: formol
3
+ Version: 0.1.0
4
+ Summary: C/C++ block comment beautifier
5
+ Home-page: https://github.com/eepp/formol/
6
+ License: MIT
7
+ Author: Philippe Proulx
8
+ Author-email: eeppeliteloop@gmail.com
9
+ Requires-Python: >=3.8,<4.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: C
15
+ Classifier: Programming Language :: C++
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Software Development :: Documentation
24
+ Project-URL: Bug tracker, https://github.com/eepp/formol/issues/
25
+ Project-URL: Repository, https://github.com/eepp/formol/
@@ -0,0 +1,678 @@
1
+ # Copyright (c) 2024 Philippe Proulx <pproulx@efficios.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+ #
22
+ # pyright: strict
23
+
24
+ from dataclasses import dataclass
25
+ from typing import List, Sequence, Union, Pattern, Match, Optional
26
+ import re
27
+
28
+
29
+ # Element base.
30
+ class _Elem:
31
+ pass
32
+
33
+ # Paragraph.
34
+ @dataclass(frozen=True)
35
+ class _Para(_Elem):
36
+ words: List[str]
37
+
38
+
39
+ # Simple list item.
40
+ @dataclass(frozen=True)
41
+ class _SimpleListItem:
42
+ elems: List[_Elem]
43
+
44
+
45
+ # Simple list base.
46
+ @dataclass(frozen=True)
47
+ class _SimpleList(_Elem):
48
+ items: List[_SimpleListItem]
49
+
50
+
51
+ # Unordered list.
52
+ class _Ul(_SimpleList):
53
+ pass
54
+
55
+
56
+ # Definition list item.
57
+ @dataclass(frozen=True)
58
+ class _DlItem:
59
+ term: str
60
+ elems: List[_Elem]
61
+
62
+
63
+ # Definition list.
64
+ @dataclass(frozen=True)
65
+ class _Dl(_Elem):
66
+ items: List[_DlItem]
67
+
68
+
69
+ # Unchanged block.
70
+ @dataclass(frozen=True)
71
+ class _AsIs(_Elem):
72
+ lines: List[str]
73
+
74
+
75
+ # Heading base.
76
+ @dataclass(frozen=True)
77
+ class _Heading(_Elem):
78
+ text: str
79
+
80
+
81
+ # Level 1 heading.
82
+ class _H1(_Heading):
83
+ pass
84
+
85
+
86
+ # Level 2 heading.
87
+ class _H2(_Heading):
88
+ pass
89
+
90
+
91
+ # Removes trailing empty lines from `lines`.
92
+ def _remove_trailing_empty_lines(lines: List[str]):
93
+ assert len(lines) >= 1
94
+
95
+ while True:
96
+ if lines[-1] != '':
97
+ break
98
+
99
+ del lines[-1]
100
+
101
+
102
+ # Returns the indentation string of `count` spaces.
103
+ def _indent_str(count: int):
104
+ return ' ' * count
105
+
106
+
107
+ # Parses the raw lines of a comment on construction; the `elems`
108
+ # property is then the resulting list of elements.
109
+ class _Parser:
110
+ def __init__(self, lines: Sequence[str]):
111
+ self._lines = lines
112
+ self._at = 0
113
+ self._last_line_index = len(lines) - 1
114
+ self._elems : List[_Elem] = []
115
+ self._parse()
116
+
117
+ # Resulting list of elements.
118
+ @property
119
+ def elems(self):
120
+ return self._elems
121
+
122
+ def _add_elem(self, elem: _Elem):
123
+ self._elems.append(elem)
124
+
125
+ @property
126
+ def _cur_line(self):
127
+ return self._lines[self._at]
128
+
129
+ @property
130
+ def _at_last_line(self):
131
+ return self._at == self._last_line_index
132
+
133
+ @property
134
+ def _prev_line(self):
135
+ if len(self._lines) == 1:
136
+ return ''
137
+
138
+ return '' if self._at == 0 else self._lines[self._at - 1]
139
+
140
+ @property
141
+ def _next_line(self):
142
+ if len(self._lines) == 1:
143
+ return ''
144
+
145
+ return '' if self._at >= self._last_line_index else self._lines[self._at + 1]
146
+
147
+ def _match_cur_line(self, pat: Union[str, Pattern[str]]) -> Optional[Match[str]]:
148
+ return re.match(pat, self._cur_line)
149
+
150
+ def _match_next_line(self, pat: Union[str, Pattern[str]]) -> Optional[Match[str]]:
151
+ return re.match(pat, self._next_line)
152
+
153
+ def _skip_empty_lines(self):
154
+ while not self._is_done:
155
+ if self._cur_line != '':
156
+ break
157
+
158
+ self._at += 1
159
+
160
+ @property
161
+ def _is_done(self):
162
+ return self._at >= len(self._lines)
163
+
164
+ # Tries to parse a level 1 heading.
165
+ def _try_parse_h1(self):
166
+ if self._next_line == '===' or self._match_next_line(r'^═+$'):
167
+ elem = _H1(self._cur_line)
168
+ self._at += 2
169
+ return elem
170
+
171
+ # Tries to parse a level 2 heading.
172
+ def _try_parse_h2(self):
173
+ if self._next_line == '---' or self._match_next_line(r'^─+$'):
174
+ elem = _H2(self._cur_line)
175
+ self._at += 2
176
+ return elem
177
+
178
+ # Unindents the lines by `count` spaces when possible, keeping
179
+ # unindentable lines as is.
180
+ @staticmethod
181
+ def _unindent_lines(lines: List[str], count: int = 4):
182
+ def unindent_line(line: str):
183
+ if line.startswith(_indent_str(count)):
184
+ # enough initial spaces to unindent
185
+ return line[count:]
186
+ else:
187
+ # keep as is
188
+ return line
189
+
190
+ return list(map(unindent_line, lines))
191
+
192
+ _indented_content_pat = re.compile(r'^ .+')
193
+
194
+ # Tries to parse a definition list item.
195
+ def _try_parse_dl_item(self):
196
+ # term?
197
+ dt_m = self._match_cur_line(r'^(\S.*):$')
198
+
199
+ if not dt_m:
200
+ # no term
201
+ return
202
+
203
+ if not self._match_next_line(self._indented_content_pat):
204
+ # no definition
205
+ return
206
+
207
+ # skip term line
208
+ self._at += 1
209
+
210
+ # parse definition lines
211
+ def_lines: List[str] = []
212
+ at = self._at
213
+
214
+ while at <= self._last_line_index:
215
+ if self._lines[at] == '':
216
+ # keep empty line
217
+ def_lines.append('')
218
+ at += 1
219
+ continue
220
+
221
+ if self._indented_content_pat.match(self._lines[at]):
222
+ # indented content line
223
+ def_lines.append(self._lines[at])
224
+ at += 1
225
+ continue
226
+
227
+ # end of definition
228
+ break
229
+
230
+ # remove trailing empty definition lines
231
+ _remove_trailing_empty_lines(def_lines)
232
+
233
+ # update current position
234
+ self._at = at
235
+
236
+ # create item from unintended definition lines
237
+ return _DlItem(dt_m.group(1),
238
+ _Parser(self._unindent_lines(def_lines)).elems)
239
+
240
+ # Tries to parse a definition list.
241
+ def _try_parse_dl(self):
242
+ items: List[_DlItem] = []
243
+
244
+ while True:
245
+ self._skip_empty_lines()
246
+
247
+ if self._is_done:
248
+ break
249
+
250
+ item = self._try_parse_dl_item()
251
+
252
+ if item is None:
253
+ break
254
+
255
+ items.append(item)
256
+
257
+ if len(items) > 0:
258
+ return _Dl(items)
259
+
260
+ # Tries to parse an unchanged block.
261
+ def _try_parse_as_is(self):
262
+ if not self._match_cur_line(self._indented_content_pat):
263
+ return
264
+
265
+ lines: List[str] = []
266
+ at = self._at
267
+
268
+ while at <= self._last_line_index:
269
+ if self._lines[at] == '':
270
+ # keep empty line
271
+ lines.append('')
272
+ at += 1
273
+ continue
274
+
275
+ if re.match(self._indented_content_pat, self._lines[at]):
276
+ # content line
277
+ lines.append(self._lines[at])
278
+ at += 1
279
+ continue
280
+
281
+ # end of block
282
+ break
283
+
284
+ # remove trailing empty lines
285
+ _remove_trailing_empty_lines(lines)
286
+
287
+ # update current position
288
+ self._at = at
289
+
290
+ # create element from unindented lines
291
+ return _AsIs(self._unindent_lines(lines))
292
+
293
+ _bullet_line_pat = re.compile(r'^(?:\*|•|‣) (.+)')
294
+
295
+ # Tries to parse an unordered list item.
296
+ def _try_parse_ul_item(self):
297
+ # item start?
298
+ bullet_m = self._match_cur_line(self._bullet_line_pat)
299
+
300
+ if not bullet_m:
301
+ # no item
302
+ return
303
+
304
+ # skip first line
305
+ self._at += 1
306
+
307
+ # parse content lines
308
+ lines: List[str] = [bullet_m.group(1)]
309
+ at = self._at
310
+
311
+ while at <= self._last_line_index:
312
+ if self._lines[at] == '':
313
+ # keep empty line
314
+ lines.append('')
315
+ at += 1
316
+ continue
317
+
318
+ if re.match(r'^ .+$', self._lines[at]):
319
+ # indented content line
320
+ lines.append(self._lines[at])
321
+ at += 1
322
+ continue
323
+
324
+ # end of item
325
+ break
326
+
327
+ # remove trailing empty lines
328
+ _remove_trailing_empty_lines(lines)
329
+
330
+ # update current position
331
+ self._at = at
332
+
333
+ # create item from unintended content lines
334
+ return _SimpleListItem(_Parser(self._unindent_lines(lines, 2)).elems)
335
+
336
+ # Tries to parse an unordered list.
337
+ def _try_parse_ul(self):
338
+ items: List[_SimpleListItem] = []
339
+
340
+ while True:
341
+ self._skip_empty_lines()
342
+
343
+ if self._is_done:
344
+ break
345
+
346
+ item = self._try_parse_ul_item()
347
+
348
+ if item is None:
349
+ break
350
+
351
+ items.append(item)
352
+
353
+ if len(items) > 0:
354
+ return _Ul(items)
355
+
356
+ _literal_pat = re.compile(r'`[^`]*`')
357
+ _word_pat = re.compile(r'(.+?)(?=`| |$)')
358
+
359
+ # Converts a paragraph string to a paragraph element containing
360
+ # individual words.
361
+ @staticmethod
362
+ def _para_elem_from_text(text: str):
363
+ elems: List[str] = []
364
+ i = 0
365
+
366
+ # scan each character
367
+ while i < len(text):
368
+ # literal?
369
+ m = _Parser._literal_pat.match(text, i)
370
+
371
+ if m:
372
+ # literal
373
+ elems.append(m.group(0))
374
+ else:
375
+ # word?
376
+ m = _Parser._word_pat.match(text, i)
377
+
378
+ if m:
379
+ # word
380
+ word = m.group(1).strip()
381
+
382
+ if len(word) > 0:
383
+ elems.append(word)
384
+
385
+ if m:
386
+ i += len(m.group(0))
387
+
388
+ return _Para(elems)
389
+
390
+ # Tries to parse a paragraph.
391
+ def _try_parse_para(self):
392
+ lines: List[str] = []
393
+
394
+ while not self._is_done:
395
+ if self._cur_line == '' or self._match_cur_line(self._bullet_line_pat):
396
+ # empty line: end of paragraph
397
+ break
398
+
399
+ lines.append(self._cur_line)
400
+ self._at += 1
401
+
402
+ if len(lines) > 0:
403
+ return self._para_elem_from_text(' '.join(lines))
404
+
405
+ def _parse(self):
406
+ while not self._is_done:
407
+ self._skip_empty_lines()
408
+
409
+ if self._is_done:
410
+ break
411
+
412
+ # level 1 heading?
413
+ elem = self._try_parse_h1()
414
+
415
+ if elem is not None:
416
+ self._add_elem(elem)
417
+ continue
418
+
419
+ # level 2 heading?
420
+ elem = self._try_parse_h2()
421
+
422
+ if elem is not None:
423
+ self._add_elem(elem)
424
+ continue
425
+
426
+ # unordered list?
427
+ elem = self._try_parse_ul()
428
+
429
+ if elem is not None:
430
+ self._add_elem(elem)
431
+ continue
432
+
433
+ # definition list?
434
+ elem = self._try_parse_dl()
435
+
436
+ if elem is not None:
437
+ self._add_elem(elem)
438
+ continue
439
+
440
+ # block as is?
441
+ elem = self._try_parse_as_is()
442
+
443
+ if elem is not None:
444
+ self._add_elem(elem)
445
+ continue
446
+
447
+ # fall back to paragraph
448
+ elem = self._try_parse_para()
449
+
450
+ if elem is not None:
451
+ self._add_elem(elem)
452
+
453
+
454
+ class _Formatter:
455
+ def __init__(self, elems: List[_Elem], max_width: int):
456
+ self._max_width = max_width
457
+ self._cur_indent = 0
458
+ self._cur_list_level = -1
459
+ self._lines = self._elems_lines(elems)
460
+
461
+ @property
462
+ def lines(self):
463
+ return self._lines
464
+
465
+ @property
466
+ def _cur_max_width(self):
467
+ return max([self._max_width - self._cur_indent, 0])
468
+
469
+ @property
470
+ def _indent_str(self):
471
+ return _indent_str(self._cur_indent)
472
+
473
+ # Returns the lines of the paragraph `para`.
474
+ def _para_lines(self, para: _Para):
475
+ lines: List[str] = [self._indent_str]
476
+
477
+ # append each word, wrapping when necessary
478
+ for word in para.words:
479
+ to_append = f'{word} '
480
+
481
+ if len(lines[-1]) + len(word) >= self._cur_max_width:
482
+ # append to current line
483
+ lines.append(self._indent_str + to_append)
484
+ else:
485
+ # new line
486
+ lines[-1] += to_append
487
+
488
+ # remove trailing empty lines
489
+ _remove_trailing_empty_lines(lines)
490
+
491
+ # append final empty line and return lines
492
+ lines.append('')
493
+ return lines
494
+
495
+ # Returns the lines of the unordered list item `item`.
496
+ def _ul_item_lines(self, item: _SimpleListItem):
497
+ # get indented element lines
498
+ self._cur_indent += 2
499
+ lines: List[str] = []
500
+
501
+ for elem in item.elems:
502
+ lines += self._elem_lines(elem)
503
+
504
+ self._cur_indent -= 2
505
+
506
+ # insert bullet point
507
+ bullet = ['•', '‣', '⁃'][self._cur_list_level % 3]
508
+ lines[0] = f'{self._indent_str}{bullet} {lines[0][self._cur_indent + 2:]}'
509
+
510
+ # remove trailing empty lines
511
+ _remove_trailing_empty_lines(lines)
512
+
513
+ # append final empty line and return lines
514
+ lines.append('')
515
+ return lines
516
+
517
+ # Returns the lines of the unordered list `ul`.
518
+ def _ul_lines(self, ul: _Ul):
519
+ lines: List[str] = []
520
+ self._cur_list_level += 1
521
+
522
+ for item in ul.items:
523
+ lines += self._ul_item_lines(item)
524
+
525
+ self._cur_list_level -= 1
526
+
527
+ # special case to make the list compact if there are only
528
+ # single-line items
529
+ if len(lines) == 2 * len(ul.items):
530
+ lines = list(filter(lambda line: len(line.strip()) > 0, lines))
531
+
532
+ # reappend final empty line
533
+ lines.append('')
534
+
535
+ return lines
536
+
537
+ # Returns the lines of the definition list item `item`.
538
+ def _dl_item_lines(self, item: _DlItem):
539
+ # start with term line
540
+ lines = [f'{item.term}:']
541
+
542
+ # get indented element lines
543
+ self._cur_indent += 4
544
+
545
+ for elem in item.elems:
546
+ lines += self._elem_lines(elem)
547
+
548
+ self._cur_indent -= 4
549
+
550
+ # remove trailing empty lines
551
+ _remove_trailing_empty_lines(lines)
552
+
553
+ # append final empty line and return lines
554
+ lines.append('')
555
+ return lines
556
+
557
+ # Returns the lines of the definition list `dl`.
558
+ def _dl_lines(self, dl: _Dl):
559
+ lines: List[str] = []
560
+
561
+ for item in dl.items:
562
+ lines += self._dl_item_lines(item)
563
+
564
+ return lines
565
+
566
+ # Returns the lines of the unchanged block `as_is`.
567
+ def _as_is_lines(self, as_is: _AsIs):
568
+ return list(map(lambda line: f'{self._indent_str} {line}',
569
+ as_is.lines)) + ['']
570
+
571
+ # Returns the lines of the level 1 heading `h1`.
572
+ def _h1_lines(self, h1: _H1):
573
+ return [
574
+ h1.text.upper(),
575
+ '═' * len(h1.text),
576
+ ]
577
+
578
+ # Returns the lines of the level 2 heading `h2`.
579
+ def _h2_lines(self, h2: _H2):
580
+ return [
581
+ h2.text,
582
+ '─' * len(h2.text),
583
+ ]
584
+
585
+ # Returns the lines of the element `elem`.
586
+ def _elem_lines(self, elem: _Elem) -> List[str]:
587
+ if type(elem) is _Para:
588
+ return self._para_lines(elem)
589
+ elif type(elem) is _Ul:
590
+ return self._ul_lines(elem)
591
+ elif type(elem) is _Dl:
592
+ return self._dl_lines(elem)
593
+ elif type(elem) is _H1:
594
+ return self._h1_lines(elem)
595
+ elif type(elem) is _H2:
596
+ return self._h2_lines(elem)
597
+ elif type(elem) is _AsIs:
598
+ return self._as_is_lines(elem)
599
+ else:
600
+ return []
601
+
602
+ # Returns the lines of the elements `elems`.
603
+ def _elems_lines(self, elems: List[_Elem]):
604
+ lines: List[str] = []
605
+
606
+ for elem in elems:
607
+ lines += self._elem_lines(elem)
608
+
609
+ # right-strip all lines
610
+ lines = list(map(lambda line: line.rstrip(), lines))
611
+
612
+ # remove trailing empty lines and return them
613
+ _remove_trailing_empty_lines(lines)
614
+ return lines
615
+
616
+
617
+ # Returns the beautified raw comment text `text` to fit on `max_width`
618
+ # columns.
619
+ #
620
+ # `text` is raw in that it must not contain special block comment
621
+ # characters, raw text.
622
+ #
623
+ # Use format_block_comment() to format a complete C/C++ block comment.
624
+ def format(text: str, max_width: int = 72):
625
+ return '\n'.join(_Formatter(_Parser(text.splitlines()).elems, max_width).lines)
626
+
627
+
628
+ # Returns the beautified version of the C/C++ block comment text `text`
629
+ # to fit on `max_width` columns.
630
+ #
631
+ # The comment text is everything between `/*` and `*/`, where the column
632
+ # of `/*` within its original document is `start_col`.
633
+ #
634
+ # The whole `comment` string must have a format such as this:
635
+ #
636
+ # /*
637
+ # * Hello world.
638
+ # * ===
639
+ # *
640
+ # * * Cupidatat in elit irure.
641
+ # * * Qui sint.
642
+ # *
643
+ # * Sunt tempor cillum ut sint.
644
+ # */
645
+ #
646
+ # The leading ` * ` strings are important.
647
+ def format_block_comment(comment: str, start_col: int = 0, max_width: int = 72):
648
+ # extract content lines from comment string
649
+ comment_lines = comment.splitlines()
650
+ content_lines: List[str] = []
651
+
652
+ for comment_line in comment_lines:
653
+ comment_line = comment_line.strip()
654
+
655
+ if comment_line in ('/*', '*/'):
656
+ continue
657
+
658
+ if comment_line == '*':
659
+ content_lines.append('')
660
+ continue
661
+
662
+ m = re.match(r'\s*\* (.+)$', comment_line)
663
+
664
+ if not m:
665
+ raise ValueError(m)
666
+
667
+ content_lines.append(m.group(1))
668
+
669
+ # format contents of comment
670
+ new_content_lines = _Formatter(_Parser(content_lines).elems, max_width).lines
671
+
672
+ # create and return final comment
673
+ new_comment_lines = ['/*']
674
+ indent_str = _indent_str(start_col)
675
+ new_comment_lines += list(map(lambda rline: f'{indent_str} * {rline}',
676
+ new_content_lines))
677
+ new_comment_lines.append(f'{indent_str} */')
678
+ return '\n'.join(new_comment_lines)
@@ -0,0 +1,50 @@
1
+ # Copyright (c) 2024 Philippe Proulx <pproulx@efficios.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ [build-system]
23
+ requires = ['poetry-core']
24
+ build-backend = 'poetry.core.masonry.api'
25
+
26
+ [tool.poetry]
27
+ name = 'formol'
28
+ version = '0.1.0'
29
+ description = 'C/C++ block comment beautifier'
30
+ license = 'MIT'
31
+ authors = ['Philippe Proulx <eeppeliteloop@gmail.com>']
32
+ repository = 'https://github.com/eepp/formol/'
33
+ classifiers = [
34
+ 'Development Status :: 4 - Beta',
35
+ 'License :: OSI Approved :: MIT License',
36
+ 'Programming Language :: Python',
37
+ 'Programming Language :: Python :: 3',
38
+ 'Intended Audience :: Developers',
39
+ 'Operating System :: OS Independent',
40
+ 'Programming Language :: C',
41
+ 'Programming Language :: C++',
42
+ 'Topic :: Software Development :: Documentation',
43
+ ]
44
+ packages = [{include = 'formol'}]
45
+
46
+ [tool.poetry.dependencies]
47
+ python = '^3.8'
48
+
49
+ [tool.poetry.urls]
50
+ 'Bug tracker' = 'https://github.com/eepp/formol/issues/'