drawlib 0.1.6__tar.gz → 0.1.7__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.

Potentially problematic release.


This version of drawlib might be problematic. Click here for more details.

Files changed (65) hide show
  1. {drawlib-0.1.6 → drawlib-0.1.7}/PKG-INFO +1 -1
  2. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/__init__.py +1 -1
  3. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/apis.py +0 -3
  4. drawlib-0.1.7/drawlib/v0_1/private/command.py +385 -0
  5. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/theme.py +83 -0
  6. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/theme_officials.py +21 -10
  7. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/settings.py +1 -1
  8. {drawlib-0.1.6 → drawlib-0.1.7}/pyproject.toml +1 -3
  9. drawlib-0.1.6/drawlib/v0_1/private/command.py +0 -204
  10. drawlib-0.1.6/drawlib/v0_1/private/tools.py +0 -161
  11. {drawlib-0.1.6 → drawlib-0.1.7}/LICENSE.txt +0 -0
  12. {drawlib-0.1.6 → drawlib-0.1.7}/README.md +0 -0
  13. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/.pylintrc +0 -0
  14. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/__main__.py +0 -0
  15. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/apis.py +0 -0
  16. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/assets/__init__.py +0 -0
  17. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/assets/v0_1/__init__.py +0 -0
  18. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/assets/v0_1/fonticons/__init__.py +0 -0
  19. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/assets/v0_1/fonts/__init__.py +0 -0
  20. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/__init__.py +0 -0
  21. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/__main__.py +0 -0
  22. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/__init__.py +0 -0
  23. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/__init__.py +0 -0
  24. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/colors.py +0 -0
  25. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/dimage.py +0 -0
  26. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/fonts.py +0 -0
  27. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/model.py +0 -0
  28. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/theme_styles.py +0 -0
  29. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core/util.py +0 -0
  30. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/__init__.py +0 -0
  31. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/base.py +0 -0
  32. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/canvas.py +0 -0
  33. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/image.py +0 -0
  34. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/line.py +0 -0
  35. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/original_arrow.py +0 -0
  36. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/original_polygon.py +0 -0
  37. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/patches.py +0 -0
  38. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/core_canvas/text.py +0 -0
  39. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/download.py +0 -0
  40. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/dutil.py +0 -0
  41. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/icons/__init__.py +0 -0
  42. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/icons/phosphor.py +0 -0
  43. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/icons/util.py +0 -0
  44. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/logging.py +0 -0
  45. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/__init__.py +0 -0
  46. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/bubblespeech.py +0 -0
  47. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/cycle.py +0 -0
  48. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/dsart.py +0 -0
  49. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/groups.py +0 -0
  50. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/pyramid.py +0 -0
  51. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/sourcecode.py +0 -0
  52. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/table.py +0 -0
  53. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/smartarts/tree.py +0 -0
  54. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/util.py +0 -0
  55. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/__init__.py +0 -0
  56. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/args.py +0 -0
  57. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/color.py +0 -0
  58. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/coordinate.py +0 -0
  59. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/image.py +0 -0
  60. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/line.py +0 -0
  61. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/shape.py +0 -0
  62. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/smartarts.py +0 -0
  63. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/style.py +0 -0
  64. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/text.py +0 -0
  65. {drawlib-0.1.6 → drawlib-0.1.7}/drawlib/v0_1/private/validators/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: drawlib
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Python drawing library. Illustration as Code.
5
5
  Home-page: https://www.drawlib.com
6
6
  Author: Yuichi Ito
@@ -24,7 +24,7 @@ subpackages contains latest and old implementations.
24
24
  from typing import Final
25
25
 
26
26
  # please update here when you release new version
27
- VERSION = "0.1.6"
27
+ VERSION = "0.1.7"
28
28
 
29
29
  # might not be changed
30
30
  LIB_NAME: Final[str] = "drawlib"
@@ -117,9 +117,6 @@ from drawlib.v0_1.private.icons import (
117
117
  ### Utilities ###
118
118
  #################
119
119
 
120
- from drawlib.v0_1.private.tools import (
121
- dexec,
122
- )
123
120
  from drawlib.v0_1.private import (
124
121
  dutil,
125
122
  )
@@ -0,0 +1,385 @@
1
+ # Copyright (c) 2024 Yuichi Ito (yuichi@yuichi.com)
2
+ #
3
+ # This software is licensed under the Apache License, Version 2.0.
4
+ # For more information, please visit: https://github.com/yuichi110/drawlib
5
+ #
6
+ # This software is provided "as is", without warranty of any kind,
7
+ # express or implied, including but not limited to the warranties of
8
+ # merchantability, fitness for a particular purpose and noninfringement.
9
+
10
+ """Handle drawlib command
11
+
12
+ Entry point ``__main__`` calls this module's function ``call()``.
13
+ After that class ``DrawlibArgParser`` parse options.
14
+ Then operate appropriate actions.
15
+
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import argparse
21
+ from typing import Optional, List, Final
22
+ import importlib.util
23
+ import traceback
24
+
25
+ import drawlib
26
+ import drawlib.v0_1
27
+ from drawlib.v0_1.private.logging import logger
28
+ from drawlib.v0_1.private.settings import dsettings
29
+ from drawlib.v0_1.private.util import (
30
+ error_handler,
31
+ get_script_relative_path,
32
+ )
33
+ from drawlib.v0_1.private.core_canvas.canvas import clear
34
+
35
+
36
+ def call() -> None:
37
+ """drawlib command handling function.
38
+
39
+ This method is called from each API's ``__main__`` module.
40
+ If the API is latest, called from root package's ``__main__`` module too.
41
+
42
+ Abstract procedure of this function.
43
+
44
+ 1. Crate DrawlibArgParser instance.
45
+ 2. Parse command line options
46
+ 3. If options requests version info, show it and quit.
47
+ 4. Apply options such as logging level
48
+ 5. get target files and directories which drawlib run.
49
+ 6. call ``tools.run()`` for each files, directories
50
+
51
+ Returns:
52
+ None
53
+
54
+ """
55
+
56
+ argparser = DrawlibArgParser()
57
+ argparser.parse()
58
+ argparser.show_version_then_exit0_if_requested()
59
+ argparser.apply_options()
60
+ paths = argparser.get_positional_args()
61
+
62
+ if len(paths) == 0:
63
+ logger.critical("no input files and directories")
64
+ logger.critical('check options with "drawlib --help"')
65
+ sys.exit(1)
66
+
67
+ for path in paths:
68
+ if not os.path.isfile(path) and not os.path.isdir(path):
69
+ msg = f'ignore arg "{path}" since it is not a file/dir path'
70
+ logger.warning(msg)
71
+ continue
72
+
73
+ abspath = os.path.abspath(path)
74
+ realpath = os.path.realpath(abspath)
75
+ executer = DrawlibExecuter()
76
+ executer.execute(realpath)
77
+
78
+
79
+ class DrawlibArgParser:
80
+ """drawlib ArgParser Class"""
81
+
82
+ def __init__(self) -> None:
83
+ """Initializa DrawlibArgParser
84
+
85
+ In this method, create ``ArgumentParser`` instance and
86
+ define command line options.
87
+
88
+ Returns:
89
+ None
90
+
91
+ """
92
+
93
+ parser = argparse.ArgumentParser(
94
+ description="Ilustration as code by python",
95
+ )
96
+ parser.add_argument(
97
+ "file_or_directory",
98
+ nargs="...",
99
+ help="Target python file or directory which contains python codes",
100
+ )
101
+ parser.add_argument(
102
+ "-v",
103
+ "--version",
104
+ action="store_true",
105
+ help="Show version",
106
+ )
107
+ parser.add_argument(
108
+ "--quiet",
109
+ action="store_true",
110
+ help="Enable quiet logging. show only error messages",
111
+ )
112
+ parser.add_argument(
113
+ "--verbose",
114
+ action="store_true",
115
+ help="Enable verbose logging.",
116
+ )
117
+ parser.add_argument(
118
+ "--developer",
119
+ action="store_true",
120
+ help=("Enable verbose logging. " "Disable error handling for library users."),
121
+ )
122
+ self._parser = parser
123
+ self._positional_args: Optional[List[str]] = None
124
+ self._name_args: Optional[argparse.Namespace] = None
125
+
126
+ def parse(self) -> None:
127
+ """Parse command line options.
128
+
129
+ Parse command line options.
130
+ This method must be called before calling get methods.
131
+
132
+ Returns:
133
+ None
134
+
135
+ """
136
+
137
+ self._name_args, _ = self._parser.parse_known_args()
138
+ args = self._parser.parse_args()
139
+ self._positional_args = args.file_or_directory
140
+
141
+ def get_positional_args(self) -> List[str]:
142
+ """Get positional args. which are files and directories.
143
+
144
+ drawlib command requires files and directories as positional args.
145
+ They are the target of ``tool.run()`` function.
146
+ It means specified file or files inside directories
147
+ are called from python.
148
+ If those python files are drawlib's one, generate images.
149
+
150
+ This method must be called after ``parse()`` method is called.
151
+
152
+ Returns:
153
+ List[str]: Positional args (files and directories)
154
+
155
+ """
156
+
157
+ if self._positional_args is None:
158
+ logger.critical("DrawlibArgParser must be parsed before getting optional args")
159
+ sys.exit(1)
160
+
161
+ return self._positional_args
162
+
163
+ def show_version_then_exit0_if_requested(self) -> None:
164
+ """show version if option ``-v/`` or ``--version`` exists. And then quit.
165
+
166
+ Check whether option has ``-v`` or ``--version``.
167
+ If yes, show version. And then, quit with ``sys.exit(0)``.
168
+
169
+ Returns:
170
+ None
171
+
172
+ """
173
+
174
+ if self._name_args is None:
175
+ logger.critical("DrawlibArgParser must be parsed before apply options")
176
+ sys.exit(1)
177
+
178
+ if self._name_args.version:
179
+ logger.critical(f"software={drawlib.VERSION}")
180
+ logger.critical(f"api={drawlib.v0_1.VERSION}")
181
+ sys.exit(0)
182
+
183
+ def apply_options(self) -> None:
184
+ """Check named args and apply appropriate configs.
185
+
186
+ Check these named argses. And do appropriate actions.
187
+
188
+ * --quiet: make logging mode quiet
189
+ * --debug: make logging mode verbose
190
+ * --devdebug: make logging mode developer
191
+
192
+ Returns:
193
+ None
194
+
195
+ """
196
+
197
+ if self._name_args is None:
198
+ logger.critical("DrawlibArgParser must be parsed before apply options")
199
+ sys.exit(1)
200
+
201
+ if self._name_args.quiet and (self._name_args.verbose or self._name_args.developer):
202
+ raise ValueError("option --quiet can't use with option --debug and --devdebug")
203
+
204
+ if self._name_args.quiet:
205
+ dsettings.set_logging_mode("quiet")
206
+
207
+ if self._name_args.verbose:
208
+ dsettings.set_logging_mode("verbose")
209
+
210
+ if self._name_args.developer:
211
+ dsettings.set_logging_mode("developer")
212
+
213
+
214
+ class DrawlibExecuter:
215
+
216
+ def __init__(self) -> None:
217
+ self._topdir_path: str = ""
218
+
219
+ @error_handler
220
+ def execute(self, file_or_directory: str, auto_clear: bool = True) -> None:
221
+ """Run specified python file or python files in specified directory
222
+
223
+ Call python file via OS path.
224
+ When you specify python file, only run it.
225
+ When you specify directory, run all python files recursively inside it.
226
+ Each python files are called only 1 time.
227
+ 2nd time call is skipped.
228
+
229
+ When file1.py calls ``run("file2.py")`` and file2.py calls
230
+ ``run("file3.py")``, loading file3.py first.
231
+ This recursive load situation can be checked on console.
232
+ Please don't make circular loading situation.
233
+
234
+ Note:
235
+ We recommend creating ``.drawlib`` directory and put all python files which are loaded.
236
+ Each drawing python file need to declare only ``run(<path_to_.drawlib>)``.
237
+
238
+ Strongly recommend ``auto_clear=True`` when specifying directory.
239
+ If user code doesn't call ``clear()``,
240
+ canvas state is shared between python files.
241
+ It means 2nd draw can contain 1st draw results.
242
+ Default value of ``auto_clear`` is True.
243
+
244
+ Args:
245
+ file_or_directory: run target. relative to user script path.
246
+ auto_clear(optional): call ``clear()`` method between running each python files.
247
+
248
+ Returns:
249
+ None
250
+
251
+ """
252
+
253
+ path = get_script_relative_path(file_or_directory)
254
+ if not os.path.exists(path):
255
+ raise ValueError(f'"{path}" does not exist')
256
+
257
+ self._add_topdir_to_syspath(path)
258
+
259
+ logger.info("Execute python files")
260
+ if os.path.isfile(path):
261
+ if not path.endswith(".py"):
262
+ raise ValueError(f'Unable to run "{path}"')
263
+ self._exec_module(path, auto_clear)
264
+ else:
265
+ file_paths = self._get_python_files(path)
266
+ for file_path in file_paths:
267
+ self._exec_module(file_path, auto_clear)
268
+
269
+ def _add_topdir_to_syspath(self, path: str):
270
+ if os.path.isfile(path):
271
+ topdir = os.path.dirname(path)
272
+ else:
273
+ init_path = os.path.join(path, "__init__.py")
274
+ if not os.path.exists(init_path):
275
+ logger.critical(f'Target directory "{path}" does not have __init__.py. Abort.')
276
+ sys.exit(1)
277
+ topdir = path
278
+
279
+ package_dir = ""
280
+ while os.path.exists(os.path.join(topdir, "__init__.py")):
281
+ package_dir = topdir
282
+ topdir = os.path.dirname(topdir)
283
+
284
+ self._topdir_path = topdir
285
+ logger.info(f'Detect package root "{package_dir}".')
286
+ if topdir not in sys.path:
287
+ sys.path.append(topdir)
288
+ logger.info(f' - Add parent directory of package root "{topdir}" to Python Path.')
289
+ else:
290
+ logger.info(f' - Parent of package directory "{topdir}" is already in Python Path.')
291
+ logger.info("")
292
+
293
+ def _get_python_files(self, directory: str) -> List[str]:
294
+ python_files: List[str] = []
295
+ for root, _, files in os.walk(directory):
296
+ for file in files:
297
+ if file.endswith(".py"):
298
+ file_path = os.path.join(root, file)
299
+ python_files.append(file_path)
300
+
301
+ sorted_files = sorted(python_files, key=lambda path: path.count(os.sep))
302
+ return sorted_files
303
+
304
+ def _exec_module(self, file_path: str, auto_clear: bool) -> None:
305
+ """write docstring later"""
306
+
307
+ words = self._get_package_words(file_path)
308
+ self._load_parent_modules(words)
309
+
310
+ if self._is_module_loaded(file_path):
311
+ logger.info(f" - {file_path}")
312
+ return
313
+
314
+ # load module
315
+ name = ".".join(words).replace(".py", "")
316
+ mspec = importlib.util.spec_from_file_location(
317
+ name=name,
318
+ location=file_path,
319
+ )
320
+ if mspec is None:
321
+ # need to investigate what situation make this
322
+ logger.info(f" - {file_path} : skipped with unknown reason.")
323
+ return
324
+ module = importlib.util.module_from_spec(mspec)
325
+
326
+ # execute and cache
327
+ try:
328
+ if auto_clear:
329
+ clear()
330
+ logger.info(f" - {file_path}")
331
+ mspec.loader.exec_module(module) # type: ignore[union-attr]
332
+ sys.modules[name] = module
333
+
334
+ except Exception as e: # pylint: disable=broad-exception-caught
335
+ # Please don't raise again.
336
+ # Error handler can't detect exact error location of imported module
337
+ file, line, _, _ = traceback.extract_tb(e.__traceback__)[-1]
338
+ logger.critical(f'{type(e).__name__} at file:"{file}", line:"{line}"')
339
+ logger.critical(str(e))
340
+ logger.debug("")
341
+ logger.debug(traceback.format_exc())
342
+ sys.exit(1)
343
+
344
+ def _is_module_loaded(self, module_path: str) -> bool:
345
+ # Normalize the module path to ensure consistent comparison
346
+ module_path = os.path.abspath(module_path)
347
+
348
+ for module_name, module in sys.modules.items():
349
+ if module is None:
350
+ continue
351
+ if not hasattr(module, "__file__"):
352
+ continue
353
+ if module.__file__ is None:
354
+ continue
355
+
356
+ loaded_module_path = os.path.abspath(module.__file__)
357
+ if loaded_module_path == module_path:
358
+ return True
359
+
360
+ return False
361
+
362
+ def _get_package_words(self, file_path):
363
+ last_path = file_path.replace(self._topdir_path, "")
364
+ words = last_path.split(os.sep)
365
+ if words[0] == "":
366
+ words = words[1:]
367
+ return words
368
+
369
+ def _load_parent_modules(self, words: List[str]):
370
+ for i in range(1, len(words)):
371
+ name = ".".join(words[:i])
372
+ if name in sys.modules:
373
+ continue
374
+
375
+ location = os.path.join(self._topdir_path, os.sep.join(words[:i]), "__init__.py")
376
+ if not os.path.exists(location):
377
+ raise ValueError(f'"{location}" does not exist. Please create it first.')
378
+
379
+ mspec = importlib.util.spec_from_file_location(
380
+ name=name,
381
+ location=location,
382
+ )
383
+ module = importlib.util.module_from_spec(mspec)
384
+ mspec.loader.exec_module(module)
385
+ sys.modules[name] = module
@@ -93,6 +93,7 @@ class Theme:
93
93
 
94
94
  @dataclasses.dataclass
95
95
  class ThemeStyle:
96
+ # mandatory for default
96
97
  backgroundcolor: Union[Tuple[int, int, int], Tuple[int, int, int, float], None] = None
97
98
  sourcecodefont: Optional[FontSourceCode] = None
98
99
  iconstyle: Optional[IconStyle] = None
@@ -103,8 +104,14 @@ class Theme:
103
104
  shapetextstyle: Optional[ShapeTextStyle] = None
104
105
  textstyle: Optional[TextStyle] = None
105
106
 
107
+ # optional for default
108
+ arcstyle: Optional[ShapeStyle] = None
109
+ arttextstyle: Optional[ShapeTextStyle] = None
110
+
106
111
  @error_handler
107
112
  def __init__(self):
113
+ self._style_names: List[str] = []
114
+
108
115
  self.backgroundcolors = BackgroundColors()
109
116
  self.sourcecodefonts = SourceCodeFonts()
110
117
  self.iconstyles = IconStyles()
@@ -174,6 +181,8 @@ class Theme:
174
181
 
175
182
  """
176
183
 
184
+ self._style_names: List[str] = []
185
+
177
186
  self.backgroundcolors = BackgroundColors()
178
187
  self.sourcecodefonts = SourceCodeFonts()
179
188
  self.iconstyles = IconStyles()
@@ -291,6 +300,8 @@ class Theme:
291
300
  # initialize
292
301
  self._initialize()
293
302
 
303
+ self._style_names.append("")
304
+
294
305
  # set default background color
295
306
  _check_color(
296
307
  default_style.backgroundcolor,
@@ -335,6 +346,8 @@ class Theme:
335
346
 
336
347
  # named styles
337
348
  for name, styles in named_styles:
349
+ self._style_names.append(name)
350
+
338
351
  if styles.backgroundcolor is not None:
339
352
  _check_color(
340
353
  styles.backgroundcolor,
@@ -383,6 +396,76 @@ class Theme:
383
396
  raise ValueError(f"name={name}, ThemeStyle.textstyle type must be TextStyle.")
384
397
  self.textstyles.set(styles.textstyle, name)
385
398
 
399
+ @error_handler
400
+ def print_style_table(self) -> None:
401
+ print(self._get_style_table())
402
+
403
+ def _get_style_table(self) -> str:
404
+ lines: List[str] = []
405
+
406
+ col1: List[str] = [
407
+ "",
408
+ "IconStyle",
409
+ "ImageStyle",
410
+ "LineStyle",
411
+ "LineArrowStyle",
412
+ "ShapeStyle",
413
+ "ShapeTextStyle",
414
+ "TextStyle",
415
+ ]
416
+
417
+ row_objects = [
418
+ self.iconstyles,
419
+ self.imagestyles,
420
+ self.linestyles,
421
+ self.linearrowstyles,
422
+ self.shapestyles,
423
+ self.shapetextstyles,
424
+ self.textstyles,
425
+ ]
426
+
427
+ # create columns
428
+ cols: List[List[str]] = [col1]
429
+ for style_name in self._style_names:
430
+ col: List[str] = [style_name]
431
+ for row_object in row_objects:
432
+ if row_object.has(style_name):
433
+ col.append("x")
434
+ else:
435
+ col.append("")
436
+ cols.append(col)
437
+
438
+ # modify text length
439
+ cols2 = []
440
+ for col in cols:
441
+ max_length = max(len(s) for s in col)
442
+ padded_strings = [s.ljust(max_length) for s in col]
443
+ cols2.append(padded_strings)
444
+
445
+ def draw_border():
446
+ text = "+"
447
+ for col in cols2:
448
+ t = "-" * (len(col[0]) + 2)
449
+ text += f"{t}+"
450
+ lines.append(text)
451
+
452
+ for i in range(len(col1)):
453
+ if i == 0:
454
+ draw_border()
455
+
456
+ text = "|"
457
+ for col in cols2:
458
+ text += f" {col[i]} |"
459
+ lines.append(text)
460
+
461
+ if i == 0:
462
+ draw_border()
463
+
464
+ if i == len(col1) - 1:
465
+ draw_border()
466
+
467
+ return "\n".join(lines).strip()
468
+
386
469
 
387
470
  def _check_color(color, error_message: str):
388
471
 
@@ -407,30 +407,41 @@ def _add_special_name_styles(
407
407
  ]
408
408
  ]:
409
409
  new_named_styles = deepcopy(named_styles)
410
- default_shapetextstyle = default_styles[5]
411
410
 
412
411
  # add white ShapeTextStyle
413
- if not isinstance(default_shapetextstyle, ShapeTextStyle):
414
- raise ValueError()
415
- white_shapetextstyle = default_shapetextstyle.copy()
416
- white_shapetextstyle.color = Colors.White
417
- new_named_styles.append(("white", [white_shapetextstyle]))
412
+ has_named_style_white = False
413
+ for named_style in new_named_styles:
414
+ name = named_style[0]
415
+ if name == "white":
416
+ has_named_style_white = True
417
+
418
+ if not has_named_style_white:
419
+ default_shapetextstyle = default_styles[5]
420
+ if not isinstance(default_shapetextstyle, ShapeTextStyle):
421
+ raise ValueError()
422
+ white_shapetextstyle = default_shapetextstyle.copy()
423
+ white_shapetextstyle.color = Colors.White
424
+ new_named_styles.append(("white", [white_shapetextstyle]))
418
425
 
419
426
  # add IconStyle and LineArrowStyle with new names
420
- for name, styles in named_styles:
427
+ inserted = 0
428
+ for i, (name, styles) in enumerate(named_styles, start=1):
421
429
  icon, image, line, linearrow, shape, shapetext, text = styles
422
430
  if isinstance(icon, IconStyle) and isinstance(shape, ShapeStyle):
423
431
  icon_lcolor = icon.copy()
424
432
  icon_lcolor.color = shape.lcolor
425
433
  icon_fcolor = icon.copy()
426
434
  icon_fcolor.color = shape.fcolor
427
- new_named_styles.append((f"lcolor-{name}", [icon_lcolor]))
428
- new_named_styles.append((f"fcolor-{name}", [icon_fcolor]))
435
+ new_named_styles.insert(i + inserted, (f"lc-{name}", [icon_lcolor]))
436
+ inserted += 1
437
+ new_named_styles.insert(i + inserted, (f"fc-{name}", [icon_fcolor]))
438
+ inserted += 1
429
439
  else:
430
440
  raise ValueError()
431
441
 
432
442
  if isinstance(linearrow, LineArrowStyle):
433
- new_named_styles.append((f"linearrow-{name}", [linearrow]))
443
+ new_named_styles.insert(i + inserted, (f"la-{name}", [linearrow]))
444
+ inserted += 1
434
445
  else:
435
446
  raise ValueError()
436
447
 
@@ -118,7 +118,7 @@ class DrawlibSettings:
118
118
  else:
119
119
  logger.critical(f'logging mode "{mode}" is not supported.')
120
120
  sys.exit(1)
121
- logger.debug(f'set_log_mode(): "{mode}"')
121
+ # logger.debug(f'set_log_mode(): "{mode}"')
122
122
 
123
123
  self._logging_mode = mode
124
124
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "drawlib"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "Python drawing library. Illustration as Code."
9
9
  homepage = "https://www.drawlib.com"
10
10
  repository = "https://github.com/yuichi110/drawlib"
@@ -29,6 +29,4 @@ radon = "^6.0.1"
29
29
  bandit = "^1.7.8"
30
30
  pytest-cov = "^5.0.0"
31
31
  pydocstyle = "^6.3.0"
32
- sphinx = "^7.2.6"
33
- sphinx-rtd-theme = "^2.0.0"
34
32
  toml = "^0.10.2"
@@ -1,204 +0,0 @@
1
- # Copyright (c) 2024 Yuichi Ito (yuichi@yuichi.com)
2
- #
3
- # This software is licensed under the Apache License, Version 2.0.
4
- # For more information, please visit: https://github.com/yuichi110/drawlib
5
- #
6
- # This software is provided "as is", without warranty of any kind,
7
- # express or implied, including but not limited to the warranties of
8
- # merchantability, fitness for a particular purpose and noninfringement.
9
-
10
- """Handle drawlib command
11
-
12
- Entry point ``__main__`` calls this module's function ``call()``.
13
- After that class ``DrawlibArgParser`` parse options.
14
- Then operate appropriate actions.
15
-
16
- """
17
-
18
- import os
19
- import sys
20
- import argparse
21
- from typing import Optional, List
22
- import drawlib
23
- import drawlib.v0_1
24
- from drawlib.v0_1.private.logging import logger
25
- from drawlib.v0_1.private.settings import dsettings
26
- from drawlib.v0_1.private.tools import dexec
27
-
28
-
29
- def call() -> None:
30
- """drawlib command handling function.
31
-
32
- This method is called from each API's ``__main__`` module.
33
- If the API is latest, called from root package's ``__main__`` module too.
34
-
35
- Abstract procedure of this function.
36
-
37
- 1. Crate DrawlibArgParser instance.
38
- 2. Parse command line options
39
- 3. If options requests version info, show it and quit.
40
- 4. Apply options such as logging level
41
- 5. get target files and directories which drawlib run.
42
- 6. call ``tools.run()`` for each files, directories
43
-
44
- Returns:
45
- None
46
-
47
- """
48
-
49
- argparser = DrawlibArgParser()
50
- argparser.parse()
51
- argparser.show_version_then_exit0_if_requested()
52
- argparser.apply_options()
53
- paths = argparser.get_positional_args()
54
-
55
- if len(paths) == 0:
56
- logger.critical("no input files and directories")
57
- logger.critical('check options with "drawlib --help"')
58
- sys.exit(1)
59
-
60
- for path in paths:
61
- if not os.path.isfile(path) and not os.path.isdir(path):
62
- msg = f'ignore arg "{path}" since it is not a file/dir path'
63
- logger.warning(msg)
64
- continue
65
-
66
- abspath = os.path.abspath(path)
67
- realpath = os.path.realpath(abspath)
68
- logger.info(realpath)
69
- dexec(realpath)
70
-
71
-
72
- class DrawlibArgParser:
73
- """drawlib ArgParser Class"""
74
-
75
- def __init__(self) -> None:
76
- """Initializa DrawlibArgParser
77
-
78
- In this method, create ``ArgumentParser`` instance and
79
- define command line options.
80
-
81
- Returns:
82
- None
83
-
84
- """
85
-
86
- parser = argparse.ArgumentParser(
87
- description="Ilustration as code by python",
88
- )
89
- parser.add_argument(
90
- "file_or_directory",
91
- nargs="...",
92
- help="Target python file or directory which contains python codes",
93
- )
94
- parser.add_argument(
95
- "-v",
96
- "--version",
97
- action="store_true",
98
- help="Show version",
99
- )
100
- parser.add_argument(
101
- "--quiet",
102
- action="store_true",
103
- help="Enable quiet logging. show only error messages",
104
- )
105
- parser.add_argument(
106
- "--verbose",
107
- action="store_true",
108
- help="Enable verbose logging.",
109
- )
110
- parser.add_argument(
111
- "--developer",
112
- action="store_true",
113
- help=("Enable verbose logging. " "Disable error handling for library users."),
114
- )
115
- self._parser = parser
116
- self._positional_args: Optional[List[str]] = None
117
- self._name_args: Optional[argparse.Namespace] = None
118
-
119
- def parse(self) -> None:
120
- """Parse command line options.
121
-
122
- Parse command line options.
123
- This method must be called before calling get methods.
124
-
125
- Returns:
126
- None
127
-
128
- """
129
-
130
- self._name_args, _ = self._parser.parse_known_args()
131
- args = self._parser.parse_args()
132
- self._positional_args = args.file_or_directory
133
-
134
- def get_positional_args(self) -> List[str]:
135
- """Get positional args. which are files and directories.
136
-
137
- drawlib command requires files and directories as positional args.
138
- They are the target of ``tool.run()`` function.
139
- It means specified file or files inside directories
140
- are called from python.
141
- If those python files are drawlib's one, generate images.
142
-
143
- This method must be called after ``parse()`` method is called.
144
-
145
- Returns:
146
- List[str]: Positional args (files and directories)
147
-
148
- """
149
-
150
- if self._positional_args is None:
151
- logger.critical("DrawlibArgParser must be parsed before getting optional args")
152
- sys.exit(1)
153
-
154
- return self._positional_args
155
-
156
- def show_version_then_exit0_if_requested(self) -> None:
157
- """show version if option ``-v/`` or ``--version`` exists. And then quit.
158
-
159
- Check whether option has ``-v`` or ``--version``.
160
- If yes, show version. And then, quit with ``sys.exit(0)``.
161
-
162
- Returns:
163
- None
164
-
165
- """
166
-
167
- if self._name_args is None:
168
- logger.critical("DrawlibArgParser must be parsed before apply options")
169
- sys.exit(1)
170
-
171
- if self._name_args.version:
172
- logger.critical(f"software={drawlib.VERSION}")
173
- logger.critical(f"api={drawlib.v0_1.VERSION}")
174
- sys.exit(0)
175
-
176
- def apply_options(self) -> None:
177
- """Check named args and apply appropriate configs.
178
-
179
- Check these named argses. And do appropriate actions.
180
-
181
- * --quiet: make logging mode quiet
182
- * --debug: make logging mode verbose
183
- * --devdebug: make logging mode developer
184
-
185
- Returns:
186
- None
187
-
188
- """
189
-
190
- if self._name_args is None:
191
- logger.critical("DrawlibArgParser must be parsed before apply options")
192
- sys.exit(1)
193
-
194
- if self._name_args.quiet and (self._name_args.verbose or self._name_args.developer):
195
- raise ValueError("option --quiet can't use with option --debug and --devdebug")
196
-
197
- if self._name_args.quiet:
198
- dsettings.set_logging_mode("quiet")
199
-
200
- if self._name_args.verbose:
201
- dsettings.set_logging_mode("verbose")
202
-
203
- if self._name_args.developer:
204
- dsettings.set_logging_mode("developer")
@@ -1,161 +0,0 @@
1
- # Copyright (c) 2024 Yuichi Ito (yuichi@yuichi.com)
2
- #
3
- # This software is licensed under the Apache License, Version 2.0.
4
- # For more information, please visit: https://github.com/yuichi110/drawlib
5
- #
6
- # This software is provided "as is", without warranty of any kind,
7
- # express or implied, including but not limited to the warranties of
8
- # merchantability, fitness for a particular purpose and noninfringement.
9
-
10
- """This module defines tool functions
11
-
12
- Note:
13
- This module's concept is very similar to util module.
14
- However, please don't merge them.
15
-
16
- This module is created for avoiding circular import between ``canvas`` and ``util``.
17
- Function ``run()`` depends on ``canvas`` and ``canvas_singleton``.
18
- And ``canvas`` and core modules depends on ``util``.
19
- If you move function ``run()`` to ``util``, it makes import trouble.
20
-
21
- """
22
-
23
- # pylint: disable=logging-fstring-interpolation
24
- # pylint: disable=global-statement
25
-
26
- import os
27
- from typing import Set, List, Final
28
- import importlib.util
29
- import traceback
30
- import sys
31
-
32
- from drawlib.v0_1.private.logging import logger
33
- from drawlib.v0_1.private.util import (
34
- error_handler,
35
- get_script_relative_path,
36
- )
37
- from drawlib.v0_1.private.core_canvas.canvas import clear
38
-
39
-
40
- class DrawlibExecuter:
41
- _INDENT_SIZE: Final[int] = 4
42
-
43
- def __init__(self) -> None:
44
- self._executed_files: Set[str] = set()
45
- self._load_stack: int = 0
46
-
47
- @error_handler
48
- def dexec(self, file_or_directory: str, auto_clear: bool = True) -> None:
49
- """Run specified python file or python files in specified directory
50
-
51
- Call python file via OS path.
52
- When you specify python file, only run it.
53
- When you specify directory, run all python files recursively inside it.
54
- Each python files are called only 1 time.
55
- 2nd time call is skipped.
56
-
57
- When file1.py calls ``run("file2.py")`` and file2.py calls
58
- ``run("file3.py")``, loading file3.py first.
59
- This recursive load situation can be checked on console.
60
- Please don't make circular loading situation.
61
-
62
- Note:
63
- We recommend creating ``.drawlib`` directory and put all python files which are loaded.
64
- Each drawing python file need to declare only ``run(<path_to_.drawlib>)``.
65
-
66
- Strongly recommend ``auto_clear=True`` when specifying directory.
67
- If user code doesn't call ``clear()``,
68
- canvas state is shared between python files.
69
- It means 2nd draw can contain 1st draw results.
70
- Default value of ``auto_clear`` is True.
71
-
72
- Args:
73
- file_or_directory: run target. relative to user script path.
74
- auto_clear(optional): call ``clear()`` method between running each python files.
75
-
76
- Returns:
77
- None
78
-
79
- """
80
-
81
- path = get_script_relative_path(file_or_directory)
82
- if not os.path.exists(path):
83
- raise ValueError(f'"{path}" does not exist')
84
-
85
- if os.path.isfile(path):
86
- if not path.endswith(".py"):
87
- raise ValueError(f'Unable to run "{path}"')
88
- self._load_file(path, auto_clear)
89
- else:
90
- self._load_directory(path, auto_clear)
91
-
92
- def _load_file(self, path: str, auto_clear: bool):
93
- self._load_stack += 1
94
- indent = " " * (self._load_stack - 1) * self._INDENT_SIZE
95
- logger.info(f"{indent}---")
96
- logger.info(f"{indent}Execute file")
97
- self._exec_module(path, auto_clear)
98
- logger.info(f"{indent}End")
99
- logger.info(f"{indent}---")
100
- self._load_stack -= 1
101
-
102
- def _load_directory(self, path: str, auto_clear: bool):
103
- self._load_stack += 1
104
- indent = " " * (self._load_stack - 1) * self._INDENT_SIZE
105
- logger.info(f"{indent}---")
106
- logger.info(f"{indent}Execute files under: {path}")
107
- file_paths = self._get_python_files(path)
108
- for file_path in file_paths:
109
- self._exec_module(file_path, auto_clear)
110
- logger.info(f"{indent}End")
111
- logger.info(f"{indent}---")
112
- self._load_stack -= 1
113
-
114
- def _get_python_files(self, directory: str) -> List[str]:
115
- python_files: List[str] = []
116
- for root, _, files in os.walk(directory):
117
- for file in files:
118
- if file.endswith(".py"):
119
- file_path = os.path.join(root, file)
120
- python_files.append(file_path)
121
-
122
- return sorted(python_files, key=lambda path: path.count(os.sep))
123
-
124
- def _exec_module(self, file_path: str, auto_clear: bool) -> None:
125
- """write docstring later"""
126
-
127
- # run only 1 time.
128
- # don't move .add() to bottom. It makes loop
129
- if file_path in self._executed_files:
130
- return
131
- self._executed_files.add(file_path)
132
-
133
- mspec = importlib.util.spec_from_file_location(
134
- name="dynamically_loaded_module",
135
- location=file_path,
136
- )
137
- if mspec is None:
138
- # need to investigate what situation
139
- return
140
- module = importlib.util.module_from_spec(mspec)
141
-
142
- try:
143
- if auto_clear:
144
- # clear previous module's drawing status
145
- clear()
146
-
147
- # call module for drawing
148
- indent = " " * (self._load_stack - 1) * self._INDENT_SIZE
149
- logger.info(f"{indent} - {file_path}")
150
- mspec.loader.exec_module(module) # type: ignore[union-attr]
151
- except Exception as e: # pylint: disable=broad-exception-caught
152
- file, line, _, _ = traceback.extract_tb(e.__traceback__)[-1]
153
- logger.critical(f'{type(e).__name__} at file:"{file}", line:"{line}"')
154
- logger.critical(str(e))
155
- logger.debug("")
156
- logger.debug(traceback.format_exc())
157
- sys.exit(1)
158
-
159
-
160
- executer = DrawlibExecuter()
161
- dexec = executer.dexec
File without changes
File without changes
File without changes
File without changes
File without changes