nelsie
1from .resources import Resources 2from .slidedeck import SlideDeck, Slide 3from .box import Box, BoxBuilderMixin, GridOptions 4from .textstyle import FontStretch, TextStyle 5from .shapes import Arrow, Path, Stroke, Rect, Point, Oval 6from .steps import StepVal 7from .stepcounter import StepCounter 8 9__all__ = [ 10 "Resources", 11 "SlideDeck", 12 "Slide", 13 "Box", 14 "BoxBuilderMixin", 15 "FontStretch", 16 "TextStyle", 17 "Arrow", 18 "Path", 19 "Rect", 20 "Point", 21 "Oval", 22 "Stroke", 23 "StepVal", 24 "GridOptions", 25 "StepCounter", 26]
11class Resources: 12 def __init__( 13 self, 14 *, 15 builtin_fonts: bool = True, 16 system_fonts: bool = False, 17 system_fonts_for_svg: bool = True, 18 default_code_syntaxes: bool = True, 19 default_code_themes: bool = True, 20 ): 21 self._resources = nelsie_rs.Resources( 22 system_fonts, 23 system_fonts_for_svg, 24 default_code_syntaxes, 25 default_code_themes, 26 ) 27 if builtin_fonts: 28 self._resources.load_fonts_dir(BUILTIN_FONTS_DIR) 29 self._resources.set_generic_family("sans-serif", "DejaVu Sans") 30 self._resources.set_generic_family("monospace", "DejaVu Sans Mono") 31 32 def set_sans_serif(self, font_name): 33 self._resources.set_generic_family("sans-serif", font_name) 34 35 def set_monospace(self, font_name): 36 self._resources.set_generic_family("monospace", font_name) 37 38 def set_serif(self, font_name): 39 self._resources.set_generic_family("serif", font_name) 40 41 def load_code_syntax_dir(self, path: str): 42 self._resources.load_code_syntax_dir(path) 43 44 def load_code_theme_dir(self, path: str): 45 self._resources.load_code_theme_dir(path) 46 47 def load_fonts_dir(self, path: str): 48 self._resources.load_fonts_dir(path) 49 50 def syntaxes(self) -> list[tuple[str, list[str]]]: 51 return self._resources.syntaxes() 52 53 def themes(self) -> list[str]: 54 return self._resources.themes()
12 def __init__( 13 self, 14 *, 15 builtin_fonts: bool = True, 16 system_fonts: bool = False, 17 system_fonts_for_svg: bool = True, 18 default_code_syntaxes: bool = True, 19 default_code_themes: bool = True, 20 ): 21 self._resources = nelsie_rs.Resources( 22 system_fonts, 23 system_fonts_for_svg, 24 default_code_syntaxes, 25 default_code_themes, 26 ) 27 if builtin_fonts: 28 self._resources.load_fonts_dir(BUILTIN_FONTS_DIR) 29 self._resources.set_generic_family("sans-serif", "DejaVu Sans") 30 self._resources.set_generic_family("monospace", "DejaVu Sans Mono")
138class SlideDeck: 139 def __init__( 140 self, 141 *, 142 width: float = 1024, 143 height: float = 768, 144 bg_color: str = "white", 145 text_style: TextStyle | None = None, 146 code_style: TextStyle = DEFAULT_CODE_STYLE, 147 resources: Resources | None = None, 148 default_code_theme: str = "InspiredGitHub", 149 default_code_language: str | None = None, 150 ): 151 """ 152 A top-level class of Nelsie. It represents a set of slides. 153 154 Arguments: 155 * width - default width of a slide (could be overridden for each slide) 156 * height - default width of a slide (could be overridden for each slide) 157 * bg_color - default background color a slide (could be overridden for each slide) 158 * resource - Resource instance, if None a new instance is created 159 * default_code_theme - Name of default theme for syntax highlighting (.code() method): 160 Available themes: 161 * "base16-ocean.dark" 162 * "base16-eighties.dark" 163 * "base16-mocha.dark" 164 * "base16-ocean.light" 165 * "InspiredGitHub" 166 * "Solarized (dark)" 167 * "Solarized (light)" 168 * default_code_language - Default language to use for syntax highlighting (.code() method) 169 """ 170 if resources is None: 171 resources = Resources() 172 else: 173 check_is_type(resources, Resources) 174 175 nelsie_rs.check_color(bg_color) 176 check_is_int_or_float(width) 177 check_is_int_or_float(height) 178 179 if text_style is not None: 180 check_is_text_style(text_style) 181 text_style = DEFAULT_TEXT_STYLE.merge(text_style) 182 else: 183 text_style = DEFAULT_TEXT_STYLE 184 185 if code_style is not None: 186 check_is_text_style(code_style) 187 188 self.width = width 189 self.height = height 190 self.bg_color = bg_color 191 self.resources = resources 192 self.default_code_theme = default_code_theme 193 self.default_code_language = default_code_language 194 self._text_styles = { 195 "default": text_style, 196 "code": code_style, 197 } 198 self.slides = [] 199 200 def get_style(self, name: str) -> TextStyle | None: 201 return self._text_styles.get(name) 202 203 def set_style(self, name: str, style: TextStyle): 204 if name == "default": 205 self.update_style(name, style) 206 return 207 check_is_str(name) 208 check_is_text_style(style) 209 self._text_styles[name] = style 210 211 def update_style(self, name: str, style: TextStyle): 212 check_is_str(name) 213 check_is_text_style(style) 214 old_style = self._text_styles.get(name) 215 if style is None: 216 self._text_styles[name] = style 217 else: 218 self._text_styles[name] = old_style.merge(style) 219 220 def new_slide( 221 self, 222 *, 223 width: Sv[float | None] = None, 224 height: Sv[float | None] = None, 225 name: str = "", 226 bg_color: Sv[str | None] = None, 227 init_steps: Iterable[Step] = (1,), 228 counters=(), 229 postprocess_fn: SlideCallback | None = None, 230 debug_steps: bool = False, 231 debug_layout: bool | str = False, 232 ): 233 if width is None: 234 width = self.width 235 if height is None: 236 height = self.height 237 if bg_color is None: 238 bg_color = self.bg_color 239 slide = Slide( 240 width, 241 height, 242 bg_color, 243 name, 244 init_steps, 245 counters, 246 postprocess_fn, 247 debug_steps, 248 debug_layout, 249 ) 250 self.slides.append(slide) 251 return slide 252 253 def slide( 254 self, 255 *, 256 width: float | None = None, 257 height: float | None = None, 258 name: str = "", 259 bg_color: str | None = None, 260 init_steps: Iterable[Step] = (1,), 261 ignore: bool = False, 262 counters: Sequence[str] = (), 263 postprocess_fn: SlideCallback | None = None, 264 debug_steps: bool = False, 265 debug_layout: bool = False, 266 ): 267 """ 268 Decorator for creating new slide. 269 It immediately calls the decorated function that should define content of the slide. 270 Slide is automatically added into the deck. 271 272 Example: 273 ```python 274 deck = SlideDeck() 275 276 @deck.slide() 277 def my_first_slide(slide): 278 slide.text("Hello!") 279 ``` 280 """ 281 282 def helper(fn): 283 if ignore: 284 return None 285 slide = self.new_slide( 286 width=width, 287 height=height, 288 bg_color=bg_color, 289 name=name, 290 init_steps=init_steps, 291 counters=counters, 292 postprocess_fn=postprocess_fn, 293 debug_steps=debug_steps, 294 debug_layout=debug_layout, 295 ) 296 fn(slide) 297 return slide 298 299 return helper 300 301 def _create_doc(self): 302 from .toraw import Document, slide_to_raw 303 from .steps_extract import extract_steps 304 305 shared_data = {} 306 raw_pages = [] 307 total_counter = CounterStorage() 308 309 slide_steps = {} 310 311 def gather_steps(slide): 312 if slide.subslides is not None: 313 for v in slide.subslides.values(): 314 for s in v: 315 gather_steps(s) 316 steps = set(slide.init_steps) 317 slide.traverse_tree(shared_data, steps) 318 extract_steps(slide, steps) 319 if slide.subslides is not None: 320 steps.update(slide.subslides.keys()) 321 if slide._extra_steps: 322 steps.update(slide._extra_steps) 323 steps = [s for s in steps if is_visible(s, slide._ignore_steps)] 324 steps.sort(key=step_compare_key) 325 slide_steps[slide] = steps 326 total_counter.increment_page(slide.counters, len(steps)) 327 total_counter.increment_slide(slide.counters) 328 329 def process_slide(slide): 330 steps = slide_steps[slide] 331 current_counter.increment_slide(slide.counters) 332 for step in steps: 333 if slide.subslides is not None: 334 parent_inserted = False 335 subslides = slide.subslides.get(step) 336 if subslides is not None: 337 for s in subslides: 338 if not parent_inserted: 339 parent_inserted = True 340 current_counter.increment_page(slide.counters) 341 page = slide_to_raw( 342 self.resources, 343 slide, 344 step, 345 self, 346 shared_data, 347 current_counter, 348 total_counter, 349 ) 350 raw_pages.append(page) 351 process_slide(s) 352 current_counter.increment_page(slide.counters) 353 page = slide_to_raw( 354 self.resources, 355 slide, 356 step, 357 self, 358 shared_data, 359 current_counter, 360 total_counter, 361 ) 362 raw_pages.append(page) 363 364 for slide in self.slides: 365 gather_steps(slide) 366 367 current_counter = CounterStorage() 368 369 for slide in self.slides: 370 process_slide(slide) 371 372 return Document(self.resources, raw_pages) 373 374 def render( 375 self, 376 path: str | None, 377 format: Literal["pdf", "png", "svg"] = "pdf", 378 *, 379 compression_level: int = 1, 380 n_threads: int | None = None, 381 progressbar: bool = True, 382 ): 383 """ 384 Render slides 385 386 If format is "pdf" then a single PDF file is created. If format is "svg" or "png" then 387 `path` specifies a directory where the slides are created as an individual files. 388 389 If `path` is None then objects are not written to the file system, and they are returned as python objects 390 from the method call. 391 392 `compression_level` defines the level of compression for PDF, allowed ranges are 0-10 393 (0 = no compression, 1 = fast compression, 10 = maximal compression) 394 """ 395 doc = self._create_doc() 396 return doc.render(path, format, compression_level, n_threads, progressbar)
139 def __init__( 140 self, 141 *, 142 width: float = 1024, 143 height: float = 768, 144 bg_color: str = "white", 145 text_style: TextStyle | None = None, 146 code_style: TextStyle = DEFAULT_CODE_STYLE, 147 resources: Resources | None = None, 148 default_code_theme: str = "InspiredGitHub", 149 default_code_language: str | None = None, 150 ): 151 """ 152 A top-level class of Nelsie. It represents a set of slides. 153 154 Arguments: 155 * width - default width of a slide (could be overridden for each slide) 156 * height - default width of a slide (could be overridden for each slide) 157 * bg_color - default background color a slide (could be overridden for each slide) 158 * resource - Resource instance, if None a new instance is created 159 * default_code_theme - Name of default theme for syntax highlighting (.code() method): 160 Available themes: 161 * "base16-ocean.dark" 162 * "base16-eighties.dark" 163 * "base16-mocha.dark" 164 * "base16-ocean.light" 165 * "InspiredGitHub" 166 * "Solarized (dark)" 167 * "Solarized (light)" 168 * default_code_language - Default language to use for syntax highlighting (.code() method) 169 """ 170 if resources is None: 171 resources = Resources() 172 else: 173 check_is_type(resources, Resources) 174 175 nelsie_rs.check_color(bg_color) 176 check_is_int_or_float(width) 177 check_is_int_or_float(height) 178 179 if text_style is not None: 180 check_is_text_style(text_style) 181 text_style = DEFAULT_TEXT_STYLE.merge(text_style) 182 else: 183 text_style = DEFAULT_TEXT_STYLE 184 185 if code_style is not None: 186 check_is_text_style(code_style) 187 188 self.width = width 189 self.height = height 190 self.bg_color = bg_color 191 self.resources = resources 192 self.default_code_theme = default_code_theme 193 self.default_code_language = default_code_language 194 self._text_styles = { 195 "default": text_style, 196 "code": code_style, 197 } 198 self.slides = []
A top-level class of Nelsie. It represents a set of slides.
Arguments:
- width - default width of a slide (could be overridden for each slide)
- height - default width of a slide (could be overridden for each slide)
- bg_color - default background color a slide (could be overridden for each slide)
- resource - Resource instance, if None a new instance is created
- default_code_theme - Name of default theme for syntax highlighting (.code() method):
Available themes:
- "base16-ocean.dark"
- "base16-eighties.dark"
- "base16-mocha.dark"
- "base16-ocean.light"
- "InspiredGitHub"
- "Solarized (dark)"
- "Solarized (light)"
- default_code_language - Default language to use for syntax highlighting (.code() method)
220 def new_slide( 221 self, 222 *, 223 width: Sv[float | None] = None, 224 height: Sv[float | None] = None, 225 name: str = "", 226 bg_color: Sv[str | None] = None, 227 init_steps: Iterable[Step] = (1,), 228 counters=(), 229 postprocess_fn: SlideCallback | None = None, 230 debug_steps: bool = False, 231 debug_layout: bool | str = False, 232 ): 233 if width is None: 234 width = self.width 235 if height is None: 236 height = self.height 237 if bg_color is None: 238 bg_color = self.bg_color 239 slide = Slide( 240 width, 241 height, 242 bg_color, 243 name, 244 init_steps, 245 counters, 246 postprocess_fn, 247 debug_steps, 248 debug_layout, 249 ) 250 self.slides.append(slide) 251 return slide
253 def slide( 254 self, 255 *, 256 width: float | None = None, 257 height: float | None = None, 258 name: str = "", 259 bg_color: str | None = None, 260 init_steps: Iterable[Step] = (1,), 261 ignore: bool = False, 262 counters: Sequence[str] = (), 263 postprocess_fn: SlideCallback | None = None, 264 debug_steps: bool = False, 265 debug_layout: bool = False, 266 ): 267 """ 268 Decorator for creating new slide. 269 It immediately calls the decorated function that should define content of the slide. 270 Slide is automatically added into the deck. 271 272 Example: 273 ```python 274 deck = SlideDeck() 275 276 @deck.slide() 277 def my_first_slide(slide): 278 slide.text("Hello!") 279 ``` 280 """ 281 282 def helper(fn): 283 if ignore: 284 return None 285 slide = self.new_slide( 286 width=width, 287 height=height, 288 bg_color=bg_color, 289 name=name, 290 init_steps=init_steps, 291 counters=counters, 292 postprocess_fn=postprocess_fn, 293 debug_steps=debug_steps, 294 debug_layout=debug_layout, 295 ) 296 fn(slide) 297 return slide 298 299 return helper
Decorator for creating new slide. It immediately calls the decorated function that should define content of the slide. Slide is automatically added into the deck.
Example:
deck = SlideDeck()
@deck.slide()
def my_first_slide(slide):
slide.text("Hello!")
374 def render( 375 self, 376 path: str | None, 377 format: Literal["pdf", "png", "svg"] = "pdf", 378 *, 379 compression_level: int = 1, 380 n_threads: int | None = None, 381 progressbar: bool = True, 382 ): 383 """ 384 Render slides 385 386 If format is "pdf" then a single PDF file is created. If format is "svg" or "png" then 387 `path` specifies a directory where the slides are created as an individual files. 388 389 If `path` is None then objects are not written to the file system, and they are returned as python objects 390 from the method call. 391 392 `compression_level` defines the level of compression for PDF, allowed ranges are 0-10 393 (0 = no compression, 1 = fast compression, 10 = maximal compression) 394 """ 395 doc = self._create_doc() 396 return doc.render(path, format, compression_level, n_threads, progressbar)
Render slides
If format is "pdf" then a single PDF file is created. If format is "svg" or "png" then
path
specifies a directory where the slides are created as an individual files.
If path
is None then objects are not written to the file system, and they are returned as python objects
from the method call.
compression_level
defines the level of compression for PDF, allowed ranges are 0-10
(0 = no compression, 1 = fast compression, 10 = maximal compression)
30class Slide(BoxBuilderMixin): 31 def __init__( 32 self, 33 width: Sv[float], 34 height: Sv[float], 35 bg_color: Sv[str], 36 name: str, 37 init_steps: Iterable[Step], 38 counters: Sequence[str], 39 postprocess_fn: SlideCallback | None = None, 40 debug_steps: bool = False, 41 debug_layout: bool | str = False, 42 ): 43 self.width = width 44 self.height = height 45 self.bg_color = bg_color 46 self.name = name 47 self.children = [] 48 self.init_steps = init_steps 49 self.counters = counters 50 self.postprocess_fn = postprocess_fn 51 self.subslides = None 52 self.debug_steps = debug_steps 53 self.debug_layout = debug_layout 54 55 self._text_styles = None 56 self._extra_steps = None 57 self._ignore_steps = None 58 59 def add(self, box): 60 self.children.append(box) 61 62 def slide_at(self, step, **kwargs): 63 def helper(fn): 64 slide = self.new_slide_at(step, **kwargs) 65 fn(slide) 66 return slide 67 68 return helper 69 70 def new_slide_at( 71 self, 72 step: Step, 73 *, 74 width: Sv[float | None] = None, 75 height: Sv[float | None] = None, 76 name: str = "", 77 bg_color: Sv[str | None] = None, 78 init_steps: Iterable[Step] = (1,), 79 counters=(), 80 postprocess_fn: SlideCallback | None = None, 81 debug_steps: bool = False, 82 debug_layout: bool | str = False, 83 ): 84 if width is None: 85 width = self.width 86 if height is None: 87 height = self.height 88 if bg_color is None: 89 bg_color = self.bg_color 90 slide = Slide( 91 width, 92 height, 93 bg_color, 94 name, 95 init_steps, 96 counters, 97 postprocess_fn, 98 debug_steps, 99 debug_layout, 100 ) 101 if self.subslides is None: 102 self.subslides = {} 103 if step not in self.subslides: 104 self.subslides[step] = [slide] 105 else: 106 self.subslides[step].append(slide) 107 return slide 108 109 def insert_step(self, step: Step): 110 if self._extra_steps is None: 111 self._extra_steps = set() 112 self._extra_steps.add(step) 113 114 def ignore_steps(self, ignored_steps: BoolStepDef): 115 self._ignore_steps = parse_bool_steps(ignored_steps) 116 117 def _set_style(self, name: str, style: Sn[TextStyle]): 118 if self._text_styles is None: 119 self._text_styles = {} 120 self._text_styles[name] = style 121 122 def _get_style(self, name: str) -> Sn[TextStyle] | None: 123 if self._text_styles is not None: 124 return self._text_styles.get(name) 125 126 def traverse_tree(self, shared_data, steps: set[Step]): 127 traverse_children(self.children, shared_data, steps) 128 129 def copy(self): 130 """ 131 Return copy of slide that is safe to add new boxes into it 132 """ 133 slide = copy(self) 134 slide.children = slide.children[:] 135 return slide
31 def __init__( 32 self, 33 width: Sv[float], 34 height: Sv[float], 35 bg_color: Sv[str], 36 name: str, 37 init_steps: Iterable[Step], 38 counters: Sequence[str], 39 postprocess_fn: SlideCallback | None = None, 40 debug_steps: bool = False, 41 debug_layout: bool | str = False, 42 ): 43 self.width = width 44 self.height = height 45 self.bg_color = bg_color 46 self.name = name 47 self.children = [] 48 self.init_steps = init_steps 49 self.counters = counters 50 self.postprocess_fn = postprocess_fn 51 self.subslides = None 52 self.debug_steps = debug_steps 53 self.debug_layout = debug_layout 54 55 self._text_styles = None 56 self._extra_steps = None 57 self._ignore_steps = None
291 def width(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 292 """ 293 Get an expression with width of the parent box. 294 """ 295 check_is_int_or_float(fraction) 296 node_id = id(self) 297 return LayoutExpr.width(node_id, fraction)
Get an expression with width of the parent box.
299 def height(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 300 """ 301 Get an expression with height of the parent box. 302 """ 303 check_is_int_or_float(fraction) 304 node_id = id(self) 305 return LayoutExpr.height(node_id, fraction)
Get an expression with height of the parent box.
70 def new_slide_at( 71 self, 72 step: Step, 73 *, 74 width: Sv[float | None] = None, 75 height: Sv[float | None] = None, 76 name: str = "", 77 bg_color: Sv[str | None] = None, 78 init_steps: Iterable[Step] = (1,), 79 counters=(), 80 postprocess_fn: SlideCallback | None = None, 81 debug_steps: bool = False, 82 debug_layout: bool | str = False, 83 ): 84 if width is None: 85 width = self.width 86 if height is None: 87 height = self.height 88 if bg_color is None: 89 bg_color = self.bg_color 90 slide = Slide( 91 width, 92 height, 93 bg_color, 94 name, 95 init_steps, 96 counters, 97 postprocess_fn, 98 debug_steps, 99 debug_layout, 100 ) 101 if self.subslides is None: 102 self.subslides = {} 103 if step not in self.subslides: 104 self.subslides[step] = [slide] 105 else: 106 self.subslides[step].append(slide) 107 return slide
432class Box(BoxBuilderMixin): 433 def __init__( 434 self, 435 *, 436 x: Sn[Position] = None, 437 y: Sn[Position] = None, 438 show: BoolStepDef = True, 439 active: BoolStepDef = True, 440 z_level: Sn[int] = None, 441 width: Sn[Size] = None, 442 height: Sn[Size] = None, 443 bg_color: Sn[str] = None, 444 row: Sv[bool] = False, 445 reverse: Sv[bool] = False, 446 p_left: Sv[Length] = 0, 447 p_right: Sv[Length] = 0, 448 p_top: Sv[Length] = 0, 449 p_bottom: Sv[Length] = 0, 450 m_left: Sv[LengthAuto] = 0, 451 m_right: Sv[LengthAuto] = 0, 452 m_top: Sv[LengthAuto] = 0, 453 m_bottom: Sv[LengthAuto] = 0, 454 flex_grow: Sv[float] = 0.0, 455 flex_shrink: Sv[float] = 1.0, 456 align_items: Sn[AlignItems] = None, 457 align_self: Sn[AlignItems] = None, 458 justify_self: Sn[AlignItems] = None, 459 align_content: Sn[AlignContent] = None, 460 justify_content: Sn[AlignContent] = None, 461 gap_x: Sv[Length] = 0, 462 gap_y: Sv[Length] = 0, 463 grid: Sn[GridOptions] = None, 464 border_radius: Sv[IntOrFloat] = 0, 465 url: Sn[str] = None, 466 name: str = "", 467 debug_layout: bool | str | None = None, 468 ): 469 """ 470 Create a new box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation. 471 """ 472 sn_check(x, check_position) 473 sn_check(y, check_position) 474 sn_check(z_level, check_is_int) 475 sn_check(width, check_size) 476 sn_check(height, check_size) 477 sn_check(bg_color, check_color) 478 sv_check(row, check_is_bool) 479 sv_check(reverse, check_is_bool) 480 sv_check(p_left, check_length) 481 sv_check(p_right, check_length) 482 sv_check(p_top, check_length) 483 sv_check(p_bottom, check_length) 484 sv_check(m_left, check_length_auto) 485 sv_check(m_right, check_length_auto) 486 sv_check(m_top, check_length_auto) 487 sv_check(m_bottom, check_length_auto) 488 489 sv_check(flex_grow, check_is_int_or_float) 490 sv_check(flex_shrink, check_is_int_or_float) 491 sv_check(gap_x, check_length) 492 sv_check(gap_y, check_length) 493 sn_check(align_items, check_align_items) 494 sn_check(align_self, check_align_items) 495 sn_check(justify_self, check_align_items) 496 sn_check(align_content, check_align_content) 497 sn_check(justify_content, check_align_content) 498 sv_check(border_radius, check_is_int_or_float) 499 sn_check(url, check_is_str) 500 check_is_str(name) 501 502 if isinstance(debug_layout, str): 503 check_color(debug_layout) 504 505 self._show = parse_bool_steps(show) 506 self._active = parse_bool_steps(active) 507 self._x = x 508 self._y = y 509 self._z_level = z_level 510 self._width = width 511 self._height = height 512 self._bg_color = bg_color 513 self._content = None 514 self._children = [] 515 self._row = row 516 self._reverse = reverse 517 self._p_left = p_left 518 self._p_right = p_right 519 self._p_top = p_top 520 self._p_bottom = p_bottom 521 self._m_left = m_left 522 self._m_right = m_right 523 self._m_top = m_top 524 self._m_bottom = m_bottom 525 526 self._flex_grow = flex_grow 527 self._flex_shrink = flex_shrink 528 self._align_items = align_items 529 self._align_self = align_self 530 self._justify_self = justify_self 531 self._align_content = align_content 532 self._justify_content = justify_content 533 self._gap_x = gap_x 534 self._gap_y = gap_y 535 self._grid = grid 536 self._debug_layout = debug_layout 537 self._border_radius = border_radius 538 self._url = url 539 self.name = name 540 self._text_styles: dict[str, Sn[TextStyle]] | None = None 541 542 def margin( 543 self, 544 all: Sn[LengthAuto] = None, 545 *, 546 x: Sn[LengthAuto] = None, 547 y: Sn[LengthAuto] = None, 548 left: Sn[LengthAuto] = None, 549 right: Sn[LengthAuto] = None, 550 top: Sn[LengthAuto] = None, 551 bottom: Sn[LengthAuto] = None, 552 ): 553 """ 554 Sets box's margin 555 """ 556 if all is not None: 557 sn_check(all, check_length_auto) 558 self._m_top = all 559 self._m_bottom = all 560 self._m_left = all 561 self._m_right = all 562 563 if x is not None: 564 sn_check(x, check_length_auto) 565 self._m_left = x 566 self._m_right = x 567 568 if y is not None: 569 sn_check(y, check_length_auto) 570 self._m_top = y 571 self._m_bottom = y 572 573 if left is not None: 574 sn_check(left, check_length_auto) 575 self._m_left = left 576 577 if right is not None: 578 sn_check(right, check_length_auto) 579 self._m_right = right 580 581 if top is not None: 582 sn_check(top, check_length_auto) 583 self._m_top = top 584 585 if bottom is not None: 586 sn_check(bottom, check_length_auto) 587 self._m_bottom = bottom 588 return self 589 590 def padding( 591 self, 592 all: Sn[LengthAuto] = None, 593 *, 594 x: Sn[LengthAuto] = None, 595 y: Sn[LengthAuto] = None, 596 left: Sn[LengthAuto] = None, 597 right: Sn[LengthAuto] = None, 598 top: Sn[LengthAuto] = None, 599 bottom: Sn[LengthAuto] = None, 600 ): 601 """ 602 Sets box's padding. 603 """ 604 if all is not None: 605 sn_check(all, check_length) 606 self._p_top = all 607 self._p_bottom = all 608 self._p_left = all 609 self._p_right = all 610 611 if x is not None: 612 sn_check(x, check_length) 613 self._p_left = x 614 self._p_right = x 615 616 if y is not None: 617 sn_check(y, check_length) 618 self._p_top = y 619 self._p_bottom = y 620 621 if left is not None: 622 sn_check(left, check_length) 623 self._p_left = left 624 625 if right is not None: 626 sn_check(right, check_length) 627 self._p_right = right 628 629 if top is not None: 630 sn_check(top, check_length) 631 self._p_top = top 632 633 if bottom is not None: 634 sn_check(bottom, check_length) 635 self._p_bottom = bottom 636 return self 637 638 def draw_line(self, p1: Point, p2: Point, **path_args): 639 """ 640 Shortcut for drawing a simple line 641 """ 642 path = Path(**path_args) 643 path.move_to(p1) 644 path.line_to(p2) 645 return self.add(path) 646 647 def add(self, item): 648 """ 649 Adds Box or a geometry item into the box 650 """ 651 self._children.append(item) 652 653 def traverse_tree(self, shared_data, steps): 654 if self._content is not None: 655 self._content.traverse_tree(shared_data, steps) 656 traverse_children(self._children, shared_data, steps) 657 658 def _set_style(self, name: str, style: Sn[TextStyle]): 659 if self._text_styles is None: 660 self._text_styles = {} 661 self._text_styles[name] = style 662 663 def _get_style(self, name: str) -> Sn[TextStyle] | None: 664 if self._text_styles is None: 665 return None 666 return self._text_styles.get(name)
433 def __init__( 434 self, 435 *, 436 x: Sn[Position] = None, 437 y: Sn[Position] = None, 438 show: BoolStepDef = True, 439 active: BoolStepDef = True, 440 z_level: Sn[int] = None, 441 width: Sn[Size] = None, 442 height: Sn[Size] = None, 443 bg_color: Sn[str] = None, 444 row: Sv[bool] = False, 445 reverse: Sv[bool] = False, 446 p_left: Sv[Length] = 0, 447 p_right: Sv[Length] = 0, 448 p_top: Sv[Length] = 0, 449 p_bottom: Sv[Length] = 0, 450 m_left: Sv[LengthAuto] = 0, 451 m_right: Sv[LengthAuto] = 0, 452 m_top: Sv[LengthAuto] = 0, 453 m_bottom: Sv[LengthAuto] = 0, 454 flex_grow: Sv[float] = 0.0, 455 flex_shrink: Sv[float] = 1.0, 456 align_items: Sn[AlignItems] = None, 457 align_self: Sn[AlignItems] = None, 458 justify_self: Sn[AlignItems] = None, 459 align_content: Sn[AlignContent] = None, 460 justify_content: Sn[AlignContent] = None, 461 gap_x: Sv[Length] = 0, 462 gap_y: Sv[Length] = 0, 463 grid: Sn[GridOptions] = None, 464 border_radius: Sv[IntOrFloat] = 0, 465 url: Sn[str] = None, 466 name: str = "", 467 debug_layout: bool | str | None = None, 468 ): 469 """ 470 Create a new box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation. 471 """ 472 sn_check(x, check_position) 473 sn_check(y, check_position) 474 sn_check(z_level, check_is_int) 475 sn_check(width, check_size) 476 sn_check(height, check_size) 477 sn_check(bg_color, check_color) 478 sv_check(row, check_is_bool) 479 sv_check(reverse, check_is_bool) 480 sv_check(p_left, check_length) 481 sv_check(p_right, check_length) 482 sv_check(p_top, check_length) 483 sv_check(p_bottom, check_length) 484 sv_check(m_left, check_length_auto) 485 sv_check(m_right, check_length_auto) 486 sv_check(m_top, check_length_auto) 487 sv_check(m_bottom, check_length_auto) 488 489 sv_check(flex_grow, check_is_int_or_float) 490 sv_check(flex_shrink, check_is_int_or_float) 491 sv_check(gap_x, check_length) 492 sv_check(gap_y, check_length) 493 sn_check(align_items, check_align_items) 494 sn_check(align_self, check_align_items) 495 sn_check(justify_self, check_align_items) 496 sn_check(align_content, check_align_content) 497 sn_check(justify_content, check_align_content) 498 sv_check(border_radius, check_is_int_or_float) 499 sn_check(url, check_is_str) 500 check_is_str(name) 501 502 if isinstance(debug_layout, str): 503 check_color(debug_layout) 504 505 self._show = parse_bool_steps(show) 506 self._active = parse_bool_steps(active) 507 self._x = x 508 self._y = y 509 self._z_level = z_level 510 self._width = width 511 self._height = height 512 self._bg_color = bg_color 513 self._content = None 514 self._children = [] 515 self._row = row 516 self._reverse = reverse 517 self._p_left = p_left 518 self._p_right = p_right 519 self._p_top = p_top 520 self._p_bottom = p_bottom 521 self._m_left = m_left 522 self._m_right = m_right 523 self._m_top = m_top 524 self._m_bottom = m_bottom 525 526 self._flex_grow = flex_grow 527 self._flex_shrink = flex_shrink 528 self._align_items = align_items 529 self._align_self = align_self 530 self._justify_self = justify_self 531 self._align_content = align_content 532 self._justify_content = justify_content 533 self._gap_x = gap_x 534 self._gap_y = gap_y 535 self._grid = grid 536 self._debug_layout = debug_layout 537 self._border_radius = border_radius 538 self._url = url 539 self.name = name 540 self._text_styles: dict[str, Sn[TextStyle]] | None = None
Create a new box. See Box reference for documentation.
542 def margin( 543 self, 544 all: Sn[LengthAuto] = None, 545 *, 546 x: Sn[LengthAuto] = None, 547 y: Sn[LengthAuto] = None, 548 left: Sn[LengthAuto] = None, 549 right: Sn[LengthAuto] = None, 550 top: Sn[LengthAuto] = None, 551 bottom: Sn[LengthAuto] = None, 552 ): 553 """ 554 Sets box's margin 555 """ 556 if all is not None: 557 sn_check(all, check_length_auto) 558 self._m_top = all 559 self._m_bottom = all 560 self._m_left = all 561 self._m_right = all 562 563 if x is not None: 564 sn_check(x, check_length_auto) 565 self._m_left = x 566 self._m_right = x 567 568 if y is not None: 569 sn_check(y, check_length_auto) 570 self._m_top = y 571 self._m_bottom = y 572 573 if left is not None: 574 sn_check(left, check_length_auto) 575 self._m_left = left 576 577 if right is not None: 578 sn_check(right, check_length_auto) 579 self._m_right = right 580 581 if top is not None: 582 sn_check(top, check_length_auto) 583 self._m_top = top 584 585 if bottom is not None: 586 sn_check(bottom, check_length_auto) 587 self._m_bottom = bottom 588 return self
Sets box's margin
590 def padding( 591 self, 592 all: Sn[LengthAuto] = None, 593 *, 594 x: Sn[LengthAuto] = None, 595 y: Sn[LengthAuto] = None, 596 left: Sn[LengthAuto] = None, 597 right: Sn[LengthAuto] = None, 598 top: Sn[LengthAuto] = None, 599 bottom: Sn[LengthAuto] = None, 600 ): 601 """ 602 Sets box's padding. 603 """ 604 if all is not None: 605 sn_check(all, check_length) 606 self._p_top = all 607 self._p_bottom = all 608 self._p_left = all 609 self._p_right = all 610 611 if x is not None: 612 sn_check(x, check_length) 613 self._p_left = x 614 self._p_right = x 615 616 if y is not None: 617 sn_check(y, check_length) 618 self._p_top = y 619 self._p_bottom = y 620 621 if left is not None: 622 sn_check(left, check_length) 623 self._p_left = left 624 625 if right is not None: 626 sn_check(right, check_length) 627 self._p_right = right 628 629 if top is not None: 630 sn_check(top, check_length) 631 self._p_top = top 632 633 if bottom is not None: 634 sn_check(bottom, check_length) 635 self._p_bottom = bottom 636 return self
Sets box's padding.
638 def draw_line(self, p1: Point, p2: Point, **path_args): 639 """ 640 Shortcut for drawing a simple line 641 """ 642 path = Path(**path_args) 643 path.move_to(p1) 644 path.line_to(p2) 645 return self.add(path)
Shortcut for drawing a simple line
59class BoxBuilderMixin: 60 def add(self, box: Union[Path, Rect, Oval, "Box"]): 61 """ 62 Adds Box or a geometry item into the box 63 """ 64 raise NotImplementedError 65 66 def _set_style(self, name: str, style: Sn[TextStyle]): 67 raise NotImplementedError 68 69 def _get_style(self, name: str) -> Sn[TextStyle] | None: 70 NotImplementedError 71 72 def box( 73 self, 74 *, 75 x: Sn[Position] = None, 76 y: Sn[Position] = None, 77 z_level: Sn[int] = None, 78 show: BoolStepDef = True, 79 active: BoolStepDef = True, 80 width: Sn[Size] = None, 81 height: Sn[Size] = None, 82 bg_color: Sn[str] = None, 83 row: Sv[bool] = False, 84 reverse: Sv[bool] = False, 85 p_left: Sv[Length] = 0, 86 p_right: Sv[Length] = 0, 87 p_top: Sv[Length] = 0, 88 p_bottom: Sv[Length] = 0, 89 m_left: Sv[LengthAuto] = 0, 90 m_right: Sv[LengthAuto] = 0, 91 m_top: Sv[LengthAuto] = 0, 92 m_bottom: Sv[LengthAuto] = 0, 93 flex_grow: Sv[float] = 0.0, 94 flex_shrink: Sv[float] = 1.0, 95 align_items: Sn[AlignItems] = None, 96 align_self: Sn[AlignItems] = None, 97 justify_self: Sn[AlignItems] = None, 98 align_content: Sn[AlignContent] = None, 99 justify_content: Sn[AlignContent] = None, 100 gap_x: Sv[Length] = 0, 101 gap_y: Sv[Length] = 0, 102 grid: Sn[GridOptions] = None, 103 border_radius: Sv[IntOrFloat] = 0, 104 url: Sn[str] = None, 105 name: str = "", 106 debug_layout: bool | str | None = None, 107 ): 108 """ 109 Create a new child box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation 110 """ 111 box = Box( 112 x=x, 113 y=y, 114 z_level=z_level, 115 show=show, 116 active=active, 117 width=width, 118 height=height, 119 bg_color=bg_color, 120 row=row, 121 reverse=reverse, 122 p_left=p_left, 123 p_right=p_right, 124 p_top=p_top, 125 p_bottom=p_bottom, 126 m_left=m_left, 127 m_right=m_right, 128 m_top=m_top, 129 m_bottom=m_bottom, 130 flex_grow=flex_grow, 131 flex_shrink=flex_shrink, 132 align_items=align_items, 133 align_self=align_self, 134 justify_self=justify_self, 135 align_content=align_content, 136 justify_content=justify_content, 137 gap_x=gap_x, 138 gap_y=gap_y, 139 grid=grid, 140 name=name, 141 border_radius=border_radius, 142 debug_layout=debug_layout, 143 url=url, 144 ) 145 self.add(box) 146 return box 147 148 def overlay(self, **box_args): 149 """ 150 Create a new box that spans over the box 151 """ 152 box_args.setdefault("x", 0) 153 box_args.setdefault("y", 0) 154 box_args.setdefault("width", "100%") 155 box_args.setdefault("height", "100%") 156 return self.box(**box_args) 157 158 def text( 159 self, 160 text: Sv[str], 161 style: Sn[TextStyle] = None, 162 *, 163 align: Sv[TextAlign] = "start", 164 strip: bool = True, 165 parse_styles: bool = True, 166 style_delimiters: str = "~{}", 167 parse_steps: bool | str = False, 168 **box_args, 169 ): 170 if strip and isinstance(text, str): 171 text = text.strip() 172 if parse_steps: 173 text = parse_steps_helper(text, parse_steps, strip) 174 sv_check(text, check_is_str) 175 sv_check(align, check_text_align) 176 sn_check(style, check_is_str_or_text_style) 177 box = self.box(**box_args) 178 box._content = TextContent( 179 text=text, 180 style=style, 181 align=align, 182 is_code=False, 183 parse_styles=parse_styles, 184 style_delimiters=style_delimiters, 185 syntax_language=None, 186 syntax_theme=None, 187 ) 188 return box 189 190 def code( 191 self, 192 text: Sv[str], 193 language: Sn[str] = None, 194 style: Sn[TextStyle] = None, 195 *, 196 align: Sv[TextAlign] = "start", 197 strip: bool = True, 198 theme: Sn[str] = None, 199 parse_styles: bool = False, 200 style_delimiters: str = "~{}", 201 parse_steps: bool | str = False, 202 **box_args, 203 ): 204 if strip and isinstance(text, str): 205 text = text.strip() 206 if parse_steps: 207 text = parse_steps_helper(text, parse_steps, strip) 208 sv_check(text, check_is_str) 209 sv_check(align, check_text_align) 210 sn_check(style, check_is_str_or_text_style) 211 sn_check(language, check_is_str) 212 sn_check(theme, check_is_str) 213 box = self.box(**box_args) 214 box._content = TextContent( 215 text=text, 216 style=style, 217 align=align, 218 is_code=True, 219 syntax_language=language, 220 syntax_theme=theme, 221 parse_styles=parse_styles, 222 style_delimiters=style_delimiters, 223 ) 224 return box 225 226 def image( 227 self, 228 path_or_data: Sn[PathOrImageData], 229 *, 230 enable_steps: Sv[bool] = True, 231 shift_steps: int = 0, 232 **box_args, 233 ): 234 sn_check(path_or_data, check_image_path_or_data) 235 sv_check(enable_steps, check_is_bool) 236 sv_check(shift_steps, check_is_int) 237 path_or_data = sn_map(path_or_data, normalize_and_watch_image_path) 238 box = self.box(**box_args) 239 box._content = ImageContent(path_or_data, enable_steps, shift_steps) 240 return box 241 242 def set_style(self, name: str, style: Sn[TextStyle]): 243 if name == "default": 244 self.update_style(name, style) 245 return 246 check_is_str(name) 247 sn_check(style, check_is_text_style) 248 self._set_style(name, style) 249 250 def update_style(self, name: str, style: TextStyle): 251 check_is_str(name) 252 check_is_text_style(style) 253 old_style = self._get_style(name) 254 if old_style is None: 255 self._set_style(name, style) 256 elif isinstance(old_style, TextStyle): 257 self._set_style(name, old_style.merge(style)) 258 else: 259 raise Exception( 260 "Non-primitive style cannot be updated; use set_style instead" 261 ) 262 263 def x(self, width_fraction: IntOrFloat = 0) -> LayoutExpr: 264 """ 265 Get an expression with X coordinate relative to the box. 266 """ 267 check_is_int_or_float(width_fraction) 268 node_id = id(self) 269 expr = LayoutExpr.x(node_id) 270 if width_fraction == 0: 271 return expr 272 return expr + LayoutExpr.width(node_id, width_fraction) 273 274 def y(self, height_fraction: IntOrFloat = 0) -> LayoutExpr: 275 """ 276 Get an expression with Y coordinate relative to the box. 277 """ 278 check_is_int_or_float(height_fraction) 279 node_id = id(self) 280 expr = LayoutExpr.y(node_id) 281 if height_fraction == 0: 282 return expr 283 return expr + LayoutExpr.height(node_id, height_fraction) 284 285 def p(self, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 286 """ 287 Get an expression with X and Y padding relative to the box. 288 """ 289 return Point(self.x(x), self.y(y)) 290 291 def width(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 292 """ 293 Get an expression with width of the parent box. 294 """ 295 check_is_int_or_float(fraction) 296 node_id = id(self) 297 return LayoutExpr.width(node_id, fraction) 298 299 def height(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 300 """ 301 Get an expression with height of the parent box. 302 """ 303 check_is_int_or_float(fraction) 304 node_id = id(self) 305 return LayoutExpr.height(node_id, fraction) 306 307 def line_x(self, line_idx: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 308 """ 309 Get an expression with X coordinate of a given line of text in the box. 310 """ 311 check_is_int_or_float(width_fraction) 312 check_is_int(line_idx) 313 node_id = id(self) 314 expr = LayoutExpr.line_x(node_id, line_idx) 315 if width_fraction == 0: 316 return expr 317 return expr + LayoutExpr.line_width(node_id, line_idx, width_fraction) 318 319 def line_y(self, line_idx: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 320 """ 321 Get an expression with Y coordinate of a given line of text in the box. 322 """ 323 check_is_int_or_float(height_fraction) 324 check_is_int(line_idx) 325 node_id = id(self) 326 expr = LayoutExpr.line_y(node_id, line_idx) 327 if height_fraction == 0: 328 return expr 329 return expr + LayoutExpr.line_height(node_id, line_idx, height_fraction) 330 331 def line_p(self, line_idx: int, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 332 return Point(self.line_x(line_idx, x), self.line_y(line_idx, y)) 333 334 def line_width(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 335 """ 336 Get an expression with a width of a given line of text in the box. 337 """ 338 check_is_int_or_float(fraction) 339 check_is_int(line_idx) 340 node_id = id(self) 341 return LayoutExpr.line_width(node_id, line_idx, fraction) 342 343 def line_height(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 344 """ 345 Get an expression with a height of a given line of text in the box. 346 """ 347 check_is_int_or_float(fraction) 348 check_is_int(line_idx) 349 node_id = id(self) 350 return LayoutExpr.line_height(node_id, line_idx, fraction) 351 352 def inline_x(self, anchor_id: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 353 """ 354 Get an expression with X coordinate of a given text anchor in the box. 355 """ 356 check_is_int_or_float(width_fraction) 357 check_is_int(anchor_id) 358 node_id = id(self) 359 expr = LayoutExpr.inline_x(node_id, anchor_id) 360 if width_fraction == 0: 361 return expr 362 return expr + LayoutExpr.inline_width(node_id, anchor_id, width_fraction) 363 364 def inline_y(self, anchor_id: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 365 """ 366 Get an expression with Y coordinate of a given text anchor in the box. 367 """ 368 check_is_int_or_float(height_fraction) 369 check_is_int(anchor_id) 370 node_id = id(self) 371 expr = LayoutExpr.inline_y(node_id, anchor_id) 372 if height_fraction == 0: 373 return expr 374 return expr + LayoutExpr.inline_height(node_id, anchor_id, height_fraction) 375 376 def inline_p(self, anchor_id: int, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 377 return Point(self.inline_x(anchor_id, x), self.inline_y(anchor_id, y)) 378 379 def inline_width(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 380 """ 381 Get an expression with a height of a given text anchor in the box. 382 """ 383 check_is_int_or_float(fraction) 384 check_is_int(anchor_id) 385 node_id = id(self) 386 return LayoutExpr.inline_width(node_id, anchor_id, fraction) 387 388 def inline_height(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 389 """ 390 Get an expression with a height of a given text anchor in the box. 391 """ 392 check_is_int_or_float(fraction) 393 check_is_int(anchor_id) 394 node_id = id(self) 395 return LayoutExpr.inline_height(node_id, anchor_id, fraction) 396 397 def line_box(self, line_idx: int, n_lines: int = 1, **box_args): 398 """ 399 Creates a new box over a text line in the box 400 """ 401 height = self.line_height(line_idx) 402 if n_lines != 1: 403 check_is_int(line_idx) 404 height = height * n_lines 405 width = LayoutExpr.max( 406 [self.line_width(line_idx + i) for i in range(n_lines)] 407 ) 408 else: 409 width = self.line_width(line_idx) 410 return self.box( 411 x=self.line_x(line_idx), 412 y=self.line_y(line_idx), 413 width=width, 414 height=height, 415 **box_args, 416 ) 417 418 def inline_box(self, anchor_id: int, **box_args): 419 """ 420 Creates a new box over a inline text anchor in the box 421 """ 422 423 return self.box( 424 x=self.inline_x(anchor_id), 425 y=self.inline_y(anchor_id), 426 width=self.inline_width(anchor_id), 427 height=self.inline_height(anchor_id), 428 **box_args, 429 )
60 def add(self, box: Union[Path, Rect, Oval, "Box"]): 61 """ 62 Adds Box or a geometry item into the box 63 """ 64 raise NotImplementedError
Adds Box or a geometry item into the box
72 def box( 73 self, 74 *, 75 x: Sn[Position] = None, 76 y: Sn[Position] = None, 77 z_level: Sn[int] = None, 78 show: BoolStepDef = True, 79 active: BoolStepDef = True, 80 width: Sn[Size] = None, 81 height: Sn[Size] = None, 82 bg_color: Sn[str] = None, 83 row: Sv[bool] = False, 84 reverse: Sv[bool] = False, 85 p_left: Sv[Length] = 0, 86 p_right: Sv[Length] = 0, 87 p_top: Sv[Length] = 0, 88 p_bottom: Sv[Length] = 0, 89 m_left: Sv[LengthAuto] = 0, 90 m_right: Sv[LengthAuto] = 0, 91 m_top: Sv[LengthAuto] = 0, 92 m_bottom: Sv[LengthAuto] = 0, 93 flex_grow: Sv[float] = 0.0, 94 flex_shrink: Sv[float] = 1.0, 95 align_items: Sn[AlignItems] = None, 96 align_self: Sn[AlignItems] = None, 97 justify_self: Sn[AlignItems] = None, 98 align_content: Sn[AlignContent] = None, 99 justify_content: Sn[AlignContent] = None, 100 gap_x: Sv[Length] = 0, 101 gap_y: Sv[Length] = 0, 102 grid: Sn[GridOptions] = None, 103 border_radius: Sv[IntOrFloat] = 0, 104 url: Sn[str] = None, 105 name: str = "", 106 debug_layout: bool | str | None = None, 107 ): 108 """ 109 Create a new child box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation 110 """ 111 box = Box( 112 x=x, 113 y=y, 114 z_level=z_level, 115 show=show, 116 active=active, 117 width=width, 118 height=height, 119 bg_color=bg_color, 120 row=row, 121 reverse=reverse, 122 p_left=p_left, 123 p_right=p_right, 124 p_top=p_top, 125 p_bottom=p_bottom, 126 m_left=m_left, 127 m_right=m_right, 128 m_top=m_top, 129 m_bottom=m_bottom, 130 flex_grow=flex_grow, 131 flex_shrink=flex_shrink, 132 align_items=align_items, 133 align_self=align_self, 134 justify_self=justify_self, 135 align_content=align_content, 136 justify_content=justify_content, 137 gap_x=gap_x, 138 gap_y=gap_y, 139 grid=grid, 140 name=name, 141 border_radius=border_radius, 142 debug_layout=debug_layout, 143 url=url, 144 ) 145 self.add(box) 146 return box
Create a new child box. See Box reference for documentation
148 def overlay(self, **box_args): 149 """ 150 Create a new box that spans over the box 151 """ 152 box_args.setdefault("x", 0) 153 box_args.setdefault("y", 0) 154 box_args.setdefault("width", "100%") 155 box_args.setdefault("height", "100%") 156 return self.box(**box_args)
Create a new box that spans over the box
158 def text( 159 self, 160 text: Sv[str], 161 style: Sn[TextStyle] = None, 162 *, 163 align: Sv[TextAlign] = "start", 164 strip: bool = True, 165 parse_styles: bool = True, 166 style_delimiters: str = "~{}", 167 parse_steps: bool | str = False, 168 **box_args, 169 ): 170 if strip and isinstance(text, str): 171 text = text.strip() 172 if parse_steps: 173 text = parse_steps_helper(text, parse_steps, strip) 174 sv_check(text, check_is_str) 175 sv_check(align, check_text_align) 176 sn_check(style, check_is_str_or_text_style) 177 box = self.box(**box_args) 178 box._content = TextContent( 179 text=text, 180 style=style, 181 align=align, 182 is_code=False, 183 parse_styles=parse_styles, 184 style_delimiters=style_delimiters, 185 syntax_language=None, 186 syntax_theme=None, 187 ) 188 return box
190 def code( 191 self, 192 text: Sv[str], 193 language: Sn[str] = None, 194 style: Sn[TextStyle] = None, 195 *, 196 align: Sv[TextAlign] = "start", 197 strip: bool = True, 198 theme: Sn[str] = None, 199 parse_styles: bool = False, 200 style_delimiters: str = "~{}", 201 parse_steps: bool | str = False, 202 **box_args, 203 ): 204 if strip and isinstance(text, str): 205 text = text.strip() 206 if parse_steps: 207 text = parse_steps_helper(text, parse_steps, strip) 208 sv_check(text, check_is_str) 209 sv_check(align, check_text_align) 210 sn_check(style, check_is_str_or_text_style) 211 sn_check(language, check_is_str) 212 sn_check(theme, check_is_str) 213 box = self.box(**box_args) 214 box._content = TextContent( 215 text=text, 216 style=style, 217 align=align, 218 is_code=True, 219 syntax_language=language, 220 syntax_theme=theme, 221 parse_styles=parse_styles, 222 style_delimiters=style_delimiters, 223 ) 224 return box
226 def image( 227 self, 228 path_or_data: Sn[PathOrImageData], 229 *, 230 enable_steps: Sv[bool] = True, 231 shift_steps: int = 0, 232 **box_args, 233 ): 234 sn_check(path_or_data, check_image_path_or_data) 235 sv_check(enable_steps, check_is_bool) 236 sv_check(shift_steps, check_is_int) 237 path_or_data = sn_map(path_or_data, normalize_and_watch_image_path) 238 box = self.box(**box_args) 239 box._content = ImageContent(path_or_data, enable_steps, shift_steps) 240 return box
250 def update_style(self, name: str, style: TextStyle): 251 check_is_str(name) 252 check_is_text_style(style) 253 old_style = self._get_style(name) 254 if old_style is None: 255 self._set_style(name, style) 256 elif isinstance(old_style, TextStyle): 257 self._set_style(name, old_style.merge(style)) 258 else: 259 raise Exception( 260 "Non-primitive style cannot be updated; use set_style instead" 261 )
263 def x(self, width_fraction: IntOrFloat = 0) -> LayoutExpr: 264 """ 265 Get an expression with X coordinate relative to the box. 266 """ 267 check_is_int_or_float(width_fraction) 268 node_id = id(self) 269 expr = LayoutExpr.x(node_id) 270 if width_fraction == 0: 271 return expr 272 return expr + LayoutExpr.width(node_id, width_fraction)
Get an expression with X coordinate relative to the box.
274 def y(self, height_fraction: IntOrFloat = 0) -> LayoutExpr: 275 """ 276 Get an expression with Y coordinate relative to the box. 277 """ 278 check_is_int_or_float(height_fraction) 279 node_id = id(self) 280 expr = LayoutExpr.y(node_id) 281 if height_fraction == 0: 282 return expr 283 return expr + LayoutExpr.height(node_id, height_fraction)
Get an expression with Y coordinate relative to the box.
285 def p(self, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 286 """ 287 Get an expression with X and Y padding relative to the box. 288 """ 289 return Point(self.x(x), self.y(y))
Get an expression with X and Y padding relative to the box.
291 def width(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 292 """ 293 Get an expression with width of the parent box. 294 """ 295 check_is_int_or_float(fraction) 296 node_id = id(self) 297 return LayoutExpr.width(node_id, fraction)
Get an expression with width of the parent box.
299 def height(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 300 """ 301 Get an expression with height of the parent box. 302 """ 303 check_is_int_or_float(fraction) 304 node_id = id(self) 305 return LayoutExpr.height(node_id, fraction)
Get an expression with height of the parent box.
307 def line_x(self, line_idx: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 308 """ 309 Get an expression with X coordinate of a given line of text in the box. 310 """ 311 check_is_int_or_float(width_fraction) 312 check_is_int(line_idx) 313 node_id = id(self) 314 expr = LayoutExpr.line_x(node_id, line_idx) 315 if width_fraction == 0: 316 return expr 317 return expr + LayoutExpr.line_width(node_id, line_idx, width_fraction)
Get an expression with X coordinate of a given line of text in the box.
319 def line_y(self, line_idx: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 320 """ 321 Get an expression with Y coordinate of a given line of text in the box. 322 """ 323 check_is_int_or_float(height_fraction) 324 check_is_int(line_idx) 325 node_id = id(self) 326 expr = LayoutExpr.line_y(node_id, line_idx) 327 if height_fraction == 0: 328 return expr 329 return expr + LayoutExpr.line_height(node_id, line_idx, height_fraction)
Get an expression with Y coordinate of a given line of text in the box.
334 def line_width(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 335 """ 336 Get an expression with a width of a given line of text in the box. 337 """ 338 check_is_int_or_float(fraction) 339 check_is_int(line_idx) 340 node_id = id(self) 341 return LayoutExpr.line_width(node_id, line_idx, fraction)
Get an expression with a width of a given line of text in the box.
343 def line_height(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 344 """ 345 Get an expression with a height of a given line of text in the box. 346 """ 347 check_is_int_or_float(fraction) 348 check_is_int(line_idx) 349 node_id = id(self) 350 return LayoutExpr.line_height(node_id, line_idx, fraction)
Get an expression with a height of a given line of text in the box.
352 def inline_x(self, anchor_id: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 353 """ 354 Get an expression with X coordinate of a given text anchor in the box. 355 """ 356 check_is_int_or_float(width_fraction) 357 check_is_int(anchor_id) 358 node_id = id(self) 359 expr = LayoutExpr.inline_x(node_id, anchor_id) 360 if width_fraction == 0: 361 return expr 362 return expr + LayoutExpr.inline_width(node_id, anchor_id, width_fraction)
Get an expression with X coordinate of a given text anchor in the box.
364 def inline_y(self, anchor_id: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 365 """ 366 Get an expression with Y coordinate of a given text anchor in the box. 367 """ 368 check_is_int_or_float(height_fraction) 369 check_is_int(anchor_id) 370 node_id = id(self) 371 expr = LayoutExpr.inline_y(node_id, anchor_id) 372 if height_fraction == 0: 373 return expr 374 return expr + LayoutExpr.inline_height(node_id, anchor_id, height_fraction)
Get an expression with Y coordinate of a given text anchor in the box.
379 def inline_width(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 380 """ 381 Get an expression with a height of a given text anchor in the box. 382 """ 383 check_is_int_or_float(fraction) 384 check_is_int(anchor_id) 385 node_id = id(self) 386 return LayoutExpr.inline_width(node_id, anchor_id, fraction)
Get an expression with a height of a given text anchor in the box.
388 def inline_height(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 389 """ 390 Get an expression with a height of a given text anchor in the box. 391 """ 392 check_is_int_or_float(fraction) 393 check_is_int(anchor_id) 394 node_id = id(self) 395 return LayoutExpr.inline_height(node_id, anchor_id, fraction)
Get an expression with a height of a given text anchor in the box.
397 def line_box(self, line_idx: int, n_lines: int = 1, **box_args): 398 """ 399 Creates a new box over a text line in the box 400 """ 401 height = self.line_height(line_idx) 402 if n_lines != 1: 403 check_is_int(line_idx) 404 height = height * n_lines 405 width = LayoutExpr.max( 406 [self.line_width(line_idx + i) for i in range(n_lines)] 407 ) 408 else: 409 width = self.line_width(line_idx) 410 return self.box( 411 x=self.line_x(line_idx), 412 y=self.line_y(line_idx), 413 width=width, 414 height=height, 415 **box_args, 416 )
Creates a new box over a text line in the box
418 def inline_box(self, anchor_id: int, **box_args): 419 """ 420 Creates a new box over a inline text anchor in the box 421 """ 422 423 return self.box( 424 x=self.inline_x(anchor_id), 425 y=self.inline_y(anchor_id), 426 width=self.inline_width(anchor_id), 427 height=self.inline_height(anchor_id), 428 **box_args, 429 )
Creates a new box over a inline text anchor in the box
10class FontStretch(IntEnum): 11 UltraCondensed = 1 12 ExtraCondensed = 2 13 Condensed = 3 14 SemiCondensed = 4 15 Normal = 5 16 SemiExpanded = 6 17 Expanded = 7 18 ExtraExpanded = 8 19 UltraExpanded = 9
34@dataclass(frozen=True) 35class TextStyle: 36 font: Sn[str] = None 37 color: Sn[str] = None 38 size: Sn[float] = None 39 line_spacing: Sn[float] = None 40 italic: Sn[bool] = None 41 stretch: Sn[FontStretch] = None 42 underline: Sn[bool] = None 43 line_through: Sn[bool] = None 44 45 # 1-1000; 400 = Normal, 700 = Bold 46 weight: Sn[int] = None 47 48 # If True, ignores weight value and forces weight 700 49 bold: Sn[bool] = None 50 51 def __post_init__(self): 52 sn_check(self.color, check_color) 53 sn_check(self.size, check_is_non_negative_int_or_float) 54 sn_check(self.line_spacing, check_is_non_negative_int_or_float) 55 sn_check(self.weight, check_is_weight) 56 57 def merge(self, other: "TextStyle") -> "TextStyle": 58 check_is_text_style(other) 59 return TextStyle( 60 *[ 61 b if b is not None else a 62 for (a, b) in zip(unpack_dataclass(self), unpack_dataclass(other)) 63 ] 64 ) 65 66 def get_step(self, step: Step) -> "TextStyle": 67 return TextStyle( 68 font=get_step(self.font, step), 69 color=get_step(self.color, step), 70 size=get_step(self.size, step), 71 line_spacing=get_step(self.line_spacing, step), 72 italic=get_step(self.italic, step), 73 stretch=get_step(self.stretch, step), 74 underline=get_step(self.underline, step), 75 line_through=get_step(self.line_through, step), 76 weight=get_step(self.weight, step), 77 bold=get_step(self.bold, step), 78 )
66 def get_step(self, step: Step) -> "TextStyle": 67 return TextStyle( 68 font=get_step(self.font, step), 69 color=get_step(self.color, step), 70 size=get_step(self.size, step), 71 line_spacing=get_step(self.line_spacing, step), 72 italic=get_step(self.italic, step), 73 stretch=get_step(self.stretch, step), 74 underline=get_step(self.underline, step), 75 line_through=get_step(self.line_through, step), 76 weight=get_step(self.weight, step), 77 bold=get_step(self.bold, step), 78 )
126@dataclass 127class Arrow: 128 """ 129 Represents an SVG arrow head. Can be attached to the start or end points of lines. 130 """ 131 132 size: Sv[float] = 10 133 """Size of the arrow head in pixels.""" 134 135 angle: Sv[float] = 40 136 """Angle of the arrow head.""" 137 138 color: Sn[str] = None 139 """Color of arrow, if None color is taken from path""" 140 141 stroke_width: Sn[float] = None 142 """If None then a filled arrow is drawn, if float then stroked arrow is drawn with the given stroke width""" 143 144 inner_point: Sn[float] = None 145 """ Shape of the arrow head. 146 147 * < 1.0 -> Sharper arrow. 148 * = 1.0 -> Normal arrow. 149 * > 1.0 -> Diamond shape arrow. 150 """ 151 152 def __post_init__(self): 153 sv_check(self.size, check_is_int_or_float) 154 sv_check(self.angle, check_is_int_or_float) 155 sn_check(self.color, check_color) 156 sn_check(self.stroke_width, check_is_int_or_float) 157 sn_check(self.inner_point, check_is_int_or_float) 158 159 def at_step(self, step: Step): 160 return Arrow( 161 get_step(self.size, step), 162 get_step(self.angle, step), 163 get_step(self.color, step), 164 get_step(self.stroke_width, step), 165 get_step(self.inner_point, step), 166 )
Represents an SVG arrow head. Can be attached to the start or end points of lines.
If None then a filled arrow is drawn, if float then stroked arrow is drawn with the given stroke width
184class Path: 185 def __init__( 186 self, 187 *, 188 stroke: Sn[Stroke] = None, 189 fill_color: Sn[str] = None, 190 arrow_start: Sn[Arrow] = None, 191 arrow_end: Sn[Arrow] = None, 192 z_level: Sn[int] = None, 193 show: BoolStepDef = True, 194 ): 195 sn_check(stroke, check_is_stroke) 196 sn_check(fill_color, check_color) 197 sn_check(arrow_start, check_is_arrow) 198 sn_check(arrow_end, check_is_arrow) 199 sn_check(z_level, check_is_int) 200 self.show = parse_bool_steps(show) 201 self.stroke = stroke 202 self.fill_color = fill_color 203 self.commands = [] 204 self.points = [] 205 self.arrow_start = arrow_start 206 self.arrow_end = arrow_end 207 self.z_level = z_level 208 209 @property 210 def last_point(self): 211 """ 212 Returns a last point in the path, if path is empty, returns 0,0 213 :return: Point 214 """ 215 if len(self.points) < 1: 216 return Point(0, 0) 217 else: 218 return self.points[-1] 219 220 def close(self): 221 self.commands.append("close") 222 return self 223 224 def move_to(self, point: Point): 225 check_is_point(point) 226 self.commands.append("move") 227 self.points.append(point) 228 return self 229 230 def move_by(self, x: PathValue, y: PathValue) -> "Path": 231 """ 232 Perform a move relative to the last point of the path. 233 If path is empty, it starts from 0,0 234 """ 235 point = self.last_point 236 237 return self.move_to(Point(point.x + x, point.y + y)) 238 239 def line_to(self, point: Point): 240 check_is_point(point) 241 self.commands.append("line") 242 self.points.append(point) 243 return self 244 245 def line_by(self, x: PathValue, y: PathValue): 246 """ 247 Draw a line relative to the last point of the path. 248 If path is empty, it starts from 0,0 249 """ 250 point = self.last_point 251 return self.line_to(Point(point.x + x, point.y + y)) 252 253 def quad_to(self, point1: Point, point: Point): 254 check_is_point(point1) 255 check_is_point(point) 256 self.commands.append("quad") 257 self.points.append(point1) 258 self.points.append(point) 259 return self 260 261 def cubic_to( 262 self, 263 point1: Point, 264 point2: Point, 265 point: Point, 266 ): 267 check_is_point(point1) 268 check_is_point(point2) 269 check_is_point(point) 270 271 self.commands.append("cubic") 272 self.points.append(point1) 273 self.points.append(point2) 274 self.points.append(point) 275 return self 276 277 def to_raw(self, step, ctx): 278 if not get_step(self.show, step, False): 279 return None 280 stroke = get_step(self.stroke, step) 281 if stroke is not None: 282 stroke = stroke.to_raw(step) 283 fill_color = get_step(self.fill_color, step) 284 285 arrow_start = get_step(self.arrow_start, step) 286 if arrow_start is not None: 287 arrow_start = arrow_start.at_step(step) 288 289 arrow_end = get_step(self.arrow_end, step) 290 if arrow_end is not None: 291 arrow_end = arrow_end.at_step(step) 292 293 points = [] 294 for p in self.points: 295 points.append(get_step(p.x, step)) 296 points.append(get_step(p.y, step)) 297 298 return RawPath( 299 stroke=stroke, 300 fill_color=fill_color, 301 arrow_start=arrow_start, 302 arrow_end=arrow_end, 303 commands=self.commands, 304 points=points, 305 z_level=get_step(self.z_level, step, ctx.z_level), 306 )
185 def __init__( 186 self, 187 *, 188 stroke: Sn[Stroke] = None, 189 fill_color: Sn[str] = None, 190 arrow_start: Sn[Arrow] = None, 191 arrow_end: Sn[Arrow] = None, 192 z_level: Sn[int] = None, 193 show: BoolStepDef = True, 194 ): 195 sn_check(stroke, check_is_stroke) 196 sn_check(fill_color, check_color) 197 sn_check(arrow_start, check_is_arrow) 198 sn_check(arrow_end, check_is_arrow) 199 sn_check(z_level, check_is_int) 200 self.show = parse_bool_steps(show) 201 self.stroke = stroke 202 self.fill_color = fill_color 203 self.commands = [] 204 self.points = [] 205 self.arrow_start = arrow_start 206 self.arrow_end = arrow_end 207 self.z_level = z_level
209 @property 210 def last_point(self): 211 """ 212 Returns a last point in the path, if path is empty, returns 0,0 213 :return: Point 214 """ 215 if len(self.points) < 1: 216 return Point(0, 0) 217 else: 218 return self.points[-1]
Returns a last point in the path, if path is empty, returns 0,0
Returns
Point
230 def move_by(self, x: PathValue, y: PathValue) -> "Path": 231 """ 232 Perform a move relative to the last point of the path. 233 If path is empty, it starts from 0,0 234 """ 235 point = self.last_point 236 237 return self.move_to(Point(point.x + x, point.y + y))
Perform a move relative to the last point of the path. If path is empty, it starts from 0,0
245 def line_by(self, x: PathValue, y: PathValue): 246 """ 247 Draw a line relative to the last point of the path. 248 If path is empty, it starts from 0,0 249 """ 250 point = self.last_point 251 return self.line_to(Point(point.x + x, point.y + y))
Draw a line relative to the last point of the path. If path is empty, it starts from 0,0
261 def cubic_to( 262 self, 263 point1: Point, 264 point2: Point, 265 point: Point, 266 ): 267 check_is_point(point1) 268 check_is_point(point2) 269 check_is_point(point) 270 271 self.commands.append("cubic") 272 self.points.append(point1) 273 self.points.append(point2) 274 self.points.append(point) 275 return self
277 def to_raw(self, step, ctx): 278 if not get_step(self.show, step, False): 279 return None 280 stroke = get_step(self.stroke, step) 281 if stroke is not None: 282 stroke = stroke.to_raw(step) 283 fill_color = get_step(self.fill_color, step) 284 285 arrow_start = get_step(self.arrow_start, step) 286 if arrow_start is not None: 287 arrow_start = arrow_start.at_step(step) 288 289 arrow_end = get_step(self.arrow_end, step) 290 if arrow_end is not None: 291 arrow_end = arrow_end.at_step(step) 292 293 points = [] 294 for p in self.points: 295 points.append(get_step(p.x, step)) 296 points.append(get_step(p.y, step)) 297 298 return RawPath( 299 stroke=stroke, 300 fill_color=fill_color, 301 arrow_start=arrow_start, 302 arrow_end=arrow_end, 303 commands=self.commands, 304 points=points, 305 z_level=get_step(self.z_level, step, ctx.z_level), 306 )
23@dataclass(frozen=True) 24class Point: 25 x: Sv[IntOrFloatOrLayoutExpr] 26 y: Sv[IntOrFloatOrLayoutExpr] 27 28 def __post_init__(self): 29 sv_check(self.x, check_position) 30 sv_check(self.y, check_position) 31 32 def move_by(self, x: IntOrFloatOrLayoutExpr, y: IntOrFloatOrLayoutExpr) -> "Point": 33 return Point(self.x + x, self.y + y)
40@dataclass(frozen=True) 41class Stroke: 42 color: Sv[str] 43 width: Sv[float] = 1.0 44 dash_array: Sn[list[float]] = None 45 dash_offset: Sv[float] = 0.0 46 47 def to_raw(self, step: Step): 48 return Stroke( 49 get_step(self.color, step), 50 get_step(self.width, step), 51 get_step(self.dash_array, step), 52 get_step(self.dash_offset, step), 53 ) 54 55 def __post_init__(self): 56 sv_check(self.color, check_color) 57 sv_check(self.width, check_is_int_or_float) 58 sv_check(self.dash_offset, check_is_int_or_float)
50class StepVal(Generic[T]): 51 def __init__( 52 self, 53 init_value: T | None = None, 54 named_steps: list[Step] | None = None, 55 init_values=None, 56 ): 57 if init_values is not None: 58 self.values = init_values 59 elif init_value is not None: 60 self.values = {1: init_value} 61 else: 62 self.values = {} 63 self.named_steps = named_steps 64 65 def at(self, step: Step, value: T) -> "StepVal[T]": 66 check_step(step) 67 self.values[step] = value 68 return self 69 70 def call(self, fn: callable(T)): 71 for value in self.values.values(): 72 fn(value) 73 74 def call_if_not_none(self, fn: callable(T)): 75 for value in self.values.values(): 76 if value is not None: 77 fn(value) 78 79 def map(self, fn: callable(T)) -> "StepVal[T]": 80 values = {} 81 for step, value in self.values.items(): 82 values[step] = fn(value) 83 return StepVal(values, self.named_steps) 84 85 def copy(self): 86 return StepVal( 87 init_values=self.values.copy(), named_steps=copy(self.named_steps) 88 ) 89 90 def get_step(self, step: Step, default_value: T | None = None) -> T | None: 91 if step in self.values: 92 return self.values[step] 93 result = default_value 94 current_step = 0 95 for i in self.values: 96 if step_lte(i, step) and step_lte(current_step, i): 97 result = self.values[i] 98 current_step = i 99 return result 100 101 def is_defined_for(self, step: Step) -> bool: 102 for i in self.values: 103 if step_lte(i, step): 104 return True 105 return False 106 107 def bool_inverse(self): 108 """Negate all values. Has meaningful output only when values are boolean.""" 109 self.values[1] = self.values.get(1, False) 110 for step in self.values: 111 self.values[step] = not self.values[step] 112 113 def __repr__(self): 114 v = "<StepVal " 115 for step, value in sorted(self.values.items(), key=step_compare_key): 116 v += f"{step}={value!r}, " 117 v += f"ns={self.named_steps!r}>" 118 return v
Abstract base class for generic types.
On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::
class Mapping[KT, VT]:
def __getitem__(self, key: KT) -> VT:
...
# Etc.
On older versions of Python, however, generic classes have to explicitly inherit from Generic.
After a class has been declared to be generic, it can then be used as follows::
def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
try:
return mapping[key]
except KeyError:
return default
51 def __init__( 52 self, 53 init_value: T | None = None, 54 named_steps: list[Step] | None = None, 55 init_values=None, 56 ): 57 if init_values is not None: 58 self.values = init_values 59 elif init_value is not None: 60 self.values = {1: init_value} 61 else: 62 self.values = {} 63 self.named_steps = named_steps
90 def get_step(self, step: Step, default_value: T | None = None) -> T | None: 91 if step in self.values: 92 return self.values[step] 93 result = default_value 94 current_step = 0 95 for i in self.values: 96 if step_lte(i, step) and step_lte(current_step, i): 97 result = self.values[i] 98 current_step = i 99 return result
107 def bool_inverse(self): 108 """Negate all values. Has meaningful output only when values are boolean.""" 109 self.values[1] = self.values.get(1, False) 110 for step in self.values: 111 self.values[step] = not self.values[step]
Negate all values. Has meaningful output only when values are boolean.
51@dataclass 52class GridOptions: 53 template_rows: Sv[GridTemplate] = () 54 template_columns: Sv[GridTemplate] = () 55 row: Sn[GridPosition] = None 56 column: Sn[GridPosition] = None
2class StepCounter: 3 def __init__(self, init_value=1): 4 self.step = init_value 5 6 def increment(self, count=1): 7 self.step += count 8 9 def set(self, step): 10 self.step = step 11 12 def last(self): 13 return self.step 14 15 def last_p(self): 16 return f"{self.step}+" 17 18 def next(self): 19 self.increment() 20 return self.step 21 22 def next_p(self): 23 self.increment() 24 return self.last_p()