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
296 def width(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 297 """ 298 Get an expression with width of the parent box. 299 """ 300 check_is_int_or_float(fraction) 301 node_id = id(self) 302 return LayoutExpr.width(node_id, fraction)
Get an expression with width of the parent box.
304 def height(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 305 """ 306 Get an expression with height of the parent box. 307 """ 308 check_is_int_or_float(fraction) 309 node_id = id(self) 310 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
437class Box(BoxBuilderMixin): 438 def __init__( 439 self, 440 *, 441 x: Sn[Position] = None, 442 y: Sn[Position] = None, 443 show: BoolStepDef = True, 444 active: BoolStepDef = True, 445 z_level: Sn[int] = None, 446 width: Sn[Size] = None, 447 height: Sn[Size] = None, 448 bg_color: Sn[str] = None, 449 row: Sv[bool] = False, 450 reverse: Sv[bool] = False, 451 p_left: Sv[Length] = 0, 452 p_right: Sv[Length] = 0, 453 p_top: Sv[Length] = 0, 454 p_bottom: Sv[Length] = 0, 455 m_left: Sv[LengthAuto] = 0, 456 m_right: Sv[LengthAuto] = 0, 457 m_top: Sv[LengthAuto] = 0, 458 m_bottom: Sv[LengthAuto] = 0, 459 flex_grow: Sv[float] = 0.0, 460 flex_shrink: Sv[float] = 1.0, 461 align_items: Sn[AlignItems] = None, 462 align_self: Sn[AlignItems] = None, 463 justify_self: Sn[AlignItems] = None, 464 align_content: Sn[AlignContent] = None, 465 justify_content: Sn[AlignContent] = None, 466 gap_x: Sv[Length] = 0, 467 gap_y: Sv[Length] = 0, 468 grid: Sn[GridOptions] = None, 469 border_radius: Sv[IntOrFloat] = 0, 470 url: Sn[str] = None, 471 name: str = "", 472 debug_layout: bool | str | None = None, 473 ): 474 """ 475 Create a new box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation. 476 """ 477 sn_check(x, check_position) 478 sn_check(y, check_position) 479 sn_check(z_level, check_is_int) 480 sn_check(width, check_size) 481 sn_check(height, check_size) 482 sn_check(bg_color, check_color) 483 sv_check(row, check_is_bool) 484 sv_check(reverse, check_is_bool) 485 sv_check(p_left, check_length) 486 sv_check(p_right, check_length) 487 sv_check(p_top, check_length) 488 sv_check(p_bottom, check_length) 489 sv_check(m_left, check_length_auto) 490 sv_check(m_right, check_length_auto) 491 sv_check(m_top, check_length_auto) 492 sv_check(m_bottom, check_length_auto) 493 494 sv_check(flex_grow, check_is_int_or_float) 495 sv_check(flex_shrink, check_is_int_or_float) 496 sv_check(gap_x, check_length) 497 sv_check(gap_y, check_length) 498 sn_check(align_items, check_align_items) 499 sn_check(align_self, check_align_items) 500 sn_check(justify_self, check_align_items) 501 sn_check(align_content, check_align_content) 502 sn_check(justify_content, check_align_content) 503 sv_check(border_radius, check_is_int_or_float) 504 sn_check(url, check_is_str) 505 check_is_str(name) 506 507 if isinstance(debug_layout, str): 508 check_color(debug_layout) 509 510 self._show = parse_bool_steps(show) 511 self._active = parse_bool_steps(active) 512 self._x = x 513 self._y = y 514 self._z_level = z_level 515 self._width = width 516 self._height = height 517 self._bg_color = bg_color 518 self._content = None 519 self._children = [] 520 self._row = row 521 self._reverse = reverse 522 self._p_left = p_left 523 self._p_right = p_right 524 self._p_top = p_top 525 self._p_bottom = p_bottom 526 self._m_left = m_left 527 self._m_right = m_right 528 self._m_top = m_top 529 self._m_bottom = m_bottom 530 531 self._flex_grow = flex_grow 532 self._flex_shrink = flex_shrink 533 self._align_items = align_items 534 self._align_self = align_self 535 self._justify_self = justify_self 536 self._align_content = align_content 537 self._justify_content = justify_content 538 self._gap_x = gap_x 539 self._gap_y = gap_y 540 self._grid = grid 541 self._debug_layout = debug_layout 542 self._border_radius = border_radius 543 self._url = url 544 self.name = name 545 self._text_styles: dict[str, Sn[TextStyle]] | None = None 546 547 def margin( 548 self, 549 all: Sn[LengthAuto] = None, 550 *, 551 x: Sn[LengthAuto] = None, 552 y: Sn[LengthAuto] = None, 553 left: Sn[LengthAuto] = None, 554 right: Sn[LengthAuto] = None, 555 top: Sn[LengthAuto] = None, 556 bottom: Sn[LengthAuto] = None, 557 ): 558 """ 559 Sets box's margin 560 """ 561 if all is not None: 562 sn_check(all, check_length_auto) 563 self._m_top = all 564 self._m_bottom = all 565 self._m_left = all 566 self._m_right = all 567 568 if x is not None: 569 sn_check(x, check_length_auto) 570 self._m_left = x 571 self._m_right = x 572 573 if y is not None: 574 sn_check(y, check_length_auto) 575 self._m_top = y 576 self._m_bottom = y 577 578 if left is not None: 579 sn_check(left, check_length_auto) 580 self._m_left = left 581 582 if right is not None: 583 sn_check(right, check_length_auto) 584 self._m_right = right 585 586 if top is not None: 587 sn_check(top, check_length_auto) 588 self._m_top = top 589 590 if bottom is not None: 591 sn_check(bottom, check_length_auto) 592 self._m_bottom = bottom 593 return self 594 595 def padding( 596 self, 597 all: Sn[LengthAuto] = None, 598 *, 599 x: Sn[LengthAuto] = None, 600 y: Sn[LengthAuto] = None, 601 left: Sn[LengthAuto] = None, 602 right: Sn[LengthAuto] = None, 603 top: Sn[LengthAuto] = None, 604 bottom: Sn[LengthAuto] = None, 605 ): 606 """ 607 Sets box's padding. 608 """ 609 if all is not None: 610 sn_check(all, check_length) 611 self._p_top = all 612 self._p_bottom = all 613 self._p_left = all 614 self._p_right = all 615 616 if x is not None: 617 sn_check(x, check_length) 618 self._p_left = x 619 self._p_right = x 620 621 if y is not None: 622 sn_check(y, check_length) 623 self._p_top = y 624 self._p_bottom = y 625 626 if left is not None: 627 sn_check(left, check_length) 628 self._p_left = left 629 630 if right is not None: 631 sn_check(right, check_length) 632 self._p_right = right 633 634 if top is not None: 635 sn_check(top, check_length) 636 self._p_top = top 637 638 if bottom is not None: 639 sn_check(bottom, check_length) 640 self._p_bottom = bottom 641 return self 642 643 def draw_line(self, p1: Point, p2: Point, **path_args): 644 """ 645 Shortcut for drawing a simple line 646 """ 647 path = Path(**path_args) 648 path.move_to(p1) 649 path.line_to(p2) 650 return self.add(path) 651 652 def add(self, item): 653 """ 654 Adds Box or a geometry item into the box 655 """ 656 self._children.append(item) 657 658 def traverse_tree(self, shared_data, steps): 659 if self._content is not None: 660 self._content.traverse_tree(shared_data, steps) 661 traverse_children(self._children, shared_data, steps) 662 663 def _set_style(self, name: str, style: Sn[TextStyle]): 664 if self._text_styles is None: 665 self._text_styles = {} 666 self._text_styles[name] = style 667 668 def _get_style(self, name: str) -> Sn[TextStyle] | None: 669 if self._text_styles is None: 670 return None 671 return self._text_styles.get(name)
438 def __init__( 439 self, 440 *, 441 x: Sn[Position] = None, 442 y: Sn[Position] = None, 443 show: BoolStepDef = True, 444 active: BoolStepDef = True, 445 z_level: Sn[int] = None, 446 width: Sn[Size] = None, 447 height: Sn[Size] = None, 448 bg_color: Sn[str] = None, 449 row: Sv[bool] = False, 450 reverse: Sv[bool] = False, 451 p_left: Sv[Length] = 0, 452 p_right: Sv[Length] = 0, 453 p_top: Sv[Length] = 0, 454 p_bottom: Sv[Length] = 0, 455 m_left: Sv[LengthAuto] = 0, 456 m_right: Sv[LengthAuto] = 0, 457 m_top: Sv[LengthAuto] = 0, 458 m_bottom: Sv[LengthAuto] = 0, 459 flex_grow: Sv[float] = 0.0, 460 flex_shrink: Sv[float] = 1.0, 461 align_items: Sn[AlignItems] = None, 462 align_self: Sn[AlignItems] = None, 463 justify_self: Sn[AlignItems] = None, 464 align_content: Sn[AlignContent] = None, 465 justify_content: Sn[AlignContent] = None, 466 gap_x: Sv[Length] = 0, 467 gap_y: Sv[Length] = 0, 468 grid: Sn[GridOptions] = None, 469 border_radius: Sv[IntOrFloat] = 0, 470 url: Sn[str] = None, 471 name: str = "", 472 debug_layout: bool | str | None = None, 473 ): 474 """ 475 Create a new box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation. 476 """ 477 sn_check(x, check_position) 478 sn_check(y, check_position) 479 sn_check(z_level, check_is_int) 480 sn_check(width, check_size) 481 sn_check(height, check_size) 482 sn_check(bg_color, check_color) 483 sv_check(row, check_is_bool) 484 sv_check(reverse, check_is_bool) 485 sv_check(p_left, check_length) 486 sv_check(p_right, check_length) 487 sv_check(p_top, check_length) 488 sv_check(p_bottom, check_length) 489 sv_check(m_left, check_length_auto) 490 sv_check(m_right, check_length_auto) 491 sv_check(m_top, check_length_auto) 492 sv_check(m_bottom, check_length_auto) 493 494 sv_check(flex_grow, check_is_int_or_float) 495 sv_check(flex_shrink, check_is_int_or_float) 496 sv_check(gap_x, check_length) 497 sv_check(gap_y, check_length) 498 sn_check(align_items, check_align_items) 499 sn_check(align_self, check_align_items) 500 sn_check(justify_self, check_align_items) 501 sn_check(align_content, check_align_content) 502 sn_check(justify_content, check_align_content) 503 sv_check(border_radius, check_is_int_or_float) 504 sn_check(url, check_is_str) 505 check_is_str(name) 506 507 if isinstance(debug_layout, str): 508 check_color(debug_layout) 509 510 self._show = parse_bool_steps(show) 511 self._active = parse_bool_steps(active) 512 self._x = x 513 self._y = y 514 self._z_level = z_level 515 self._width = width 516 self._height = height 517 self._bg_color = bg_color 518 self._content = None 519 self._children = [] 520 self._row = row 521 self._reverse = reverse 522 self._p_left = p_left 523 self._p_right = p_right 524 self._p_top = p_top 525 self._p_bottom = p_bottom 526 self._m_left = m_left 527 self._m_right = m_right 528 self._m_top = m_top 529 self._m_bottom = m_bottom 530 531 self._flex_grow = flex_grow 532 self._flex_shrink = flex_shrink 533 self._align_items = align_items 534 self._align_self = align_self 535 self._justify_self = justify_self 536 self._align_content = align_content 537 self._justify_content = justify_content 538 self._gap_x = gap_x 539 self._gap_y = gap_y 540 self._grid = grid 541 self._debug_layout = debug_layout 542 self._border_radius = border_radius 543 self._url = url 544 self.name = name 545 self._text_styles: dict[str, Sn[TextStyle]] | None = None
Create a new box. See Box reference for documentation.
547 def margin( 548 self, 549 all: Sn[LengthAuto] = None, 550 *, 551 x: Sn[LengthAuto] = None, 552 y: Sn[LengthAuto] = None, 553 left: Sn[LengthAuto] = None, 554 right: Sn[LengthAuto] = None, 555 top: Sn[LengthAuto] = None, 556 bottom: Sn[LengthAuto] = None, 557 ): 558 """ 559 Sets box's margin 560 """ 561 if all is not None: 562 sn_check(all, check_length_auto) 563 self._m_top = all 564 self._m_bottom = all 565 self._m_left = all 566 self._m_right = all 567 568 if x is not None: 569 sn_check(x, check_length_auto) 570 self._m_left = x 571 self._m_right = x 572 573 if y is not None: 574 sn_check(y, check_length_auto) 575 self._m_top = y 576 self._m_bottom = y 577 578 if left is not None: 579 sn_check(left, check_length_auto) 580 self._m_left = left 581 582 if right is not None: 583 sn_check(right, check_length_auto) 584 self._m_right = right 585 586 if top is not None: 587 sn_check(top, check_length_auto) 588 self._m_top = top 589 590 if bottom is not None: 591 sn_check(bottom, check_length_auto) 592 self._m_bottom = bottom 593 return self
Sets box's margin
595 def padding( 596 self, 597 all: Sn[LengthAuto] = None, 598 *, 599 x: Sn[LengthAuto] = None, 600 y: Sn[LengthAuto] = None, 601 left: Sn[LengthAuto] = None, 602 right: Sn[LengthAuto] = None, 603 top: Sn[LengthAuto] = None, 604 bottom: Sn[LengthAuto] = None, 605 ): 606 """ 607 Sets box's padding. 608 """ 609 if all is not None: 610 sn_check(all, check_length) 611 self._p_top = all 612 self._p_bottom = all 613 self._p_left = all 614 self._p_right = all 615 616 if x is not None: 617 sn_check(x, check_length) 618 self._p_left = x 619 self._p_right = x 620 621 if y is not None: 622 sn_check(y, check_length) 623 self._p_top = y 624 self._p_bottom = y 625 626 if left is not None: 627 sn_check(left, check_length) 628 self._p_left = left 629 630 if right is not None: 631 sn_check(right, check_length) 632 self._p_right = right 633 634 if top is not None: 635 sn_check(top, check_length) 636 self._p_top = top 637 638 if bottom is not None: 639 sn_check(bottom, check_length) 640 self._p_bottom = bottom 641 return self
Sets box's padding.
643 def draw_line(self, p1: Point, p2: Point, **path_args): 644 """ 645 Shortcut for drawing a simple line 646 """ 647 path = Path(**path_args) 648 path.move_to(p1) 649 path.line_to(p2) 650 return self.add(path)
Shortcut for drawing a simple line
60class BoxBuilderMixin: 61 def add(self, box: Union[Path, Rect, Oval, "Box"]): 62 """ 63 Adds Box or a geometry item into the box 64 """ 65 raise NotImplementedError 66 67 def _set_style(self, name: str, style: Sn[TextStyle]): 68 raise NotImplementedError 69 70 def _get_style(self, name: str) -> Sn[TextStyle] | None: 71 NotImplementedError 72 73 def box( 74 self, 75 *, 76 x: Sn[Position] = None, 77 y: Sn[Position] = None, 78 z_level: Sn[int] = None, 79 show: BoolStepDef = True, 80 active: BoolStepDef = True, 81 width: Sn[Size] = None, 82 height: Sn[Size] = None, 83 bg_color: Sn[str] = None, 84 row: Sv[bool] = False, 85 reverse: Sv[bool] = False, 86 p_left: Sv[Length] = 0, 87 p_right: Sv[Length] = 0, 88 p_top: Sv[Length] = 0, 89 p_bottom: Sv[Length] = 0, 90 m_left: Sv[LengthAuto] = 0, 91 m_right: Sv[LengthAuto] = 0, 92 m_top: Sv[LengthAuto] = 0, 93 m_bottom: Sv[LengthAuto] = 0, 94 flex_grow: Sv[float] = 0.0, 95 flex_shrink: Sv[float] = 1.0, 96 align_items: Sn[AlignItems] = None, 97 align_self: Sn[AlignItems] = None, 98 justify_self: Sn[AlignItems] = None, 99 align_content: Sn[AlignContent] = None, 100 justify_content: Sn[AlignContent] = None, 101 gap_x: Sv[Length] = 0, 102 gap_y: Sv[Length] = 0, 103 grid: Sn[GridOptions] = None, 104 border_radius: Sv[IntOrFloat] = 0, 105 url: Sn[str] = None, 106 name: str = "", 107 debug_layout: bool | str | None = None, 108 ): 109 """ 110 Create a new child box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation 111 """ 112 box = Box( 113 x=x, 114 y=y, 115 z_level=z_level, 116 show=show, 117 active=active, 118 width=width, 119 height=height, 120 bg_color=bg_color, 121 row=row, 122 reverse=reverse, 123 p_left=p_left, 124 p_right=p_right, 125 p_top=p_top, 126 p_bottom=p_bottom, 127 m_left=m_left, 128 m_right=m_right, 129 m_top=m_top, 130 m_bottom=m_bottom, 131 flex_grow=flex_grow, 132 flex_shrink=flex_shrink, 133 align_items=align_items, 134 align_self=align_self, 135 justify_self=justify_self, 136 align_content=align_content, 137 justify_content=justify_content, 138 gap_x=gap_x, 139 gap_y=gap_y, 140 grid=grid, 141 name=name, 142 border_radius=border_radius, 143 debug_layout=debug_layout, 144 url=url, 145 ) 146 self.add(box) 147 return box 148 149 def overlay(self, **box_args): 150 """ 151 Create a new box that spans over the box 152 """ 153 box_args.setdefault("x", 0) 154 box_args.setdefault("y", 0) 155 box_args.setdefault("width", "100%") 156 box_args.setdefault("height", "100%") 157 return self.box(**box_args) 158 159 def text( 160 self, 161 text: Sv[str], 162 style: Sn[TextStyle] = None, 163 *, 164 align: Sv[TextAlign] = "start", 165 strip: bool = True, 166 parse_styles: bool = True, 167 style_delimiters: str = "~{}", 168 parse_steps: bool | str = False, 169 **box_args, 170 ): 171 if strip and isinstance(text, str): 172 text = text.strip() 173 if parse_steps: 174 text = parse_steps_helper(text, parse_steps, strip) 175 sv_check(text, check_is_str) 176 sv_check(align, check_text_align) 177 sn_check(style, check_is_str_or_text_style) 178 box = self.box(**box_args) 179 box._content = TextContent( 180 text=text, 181 style=style, 182 align=align, 183 is_code=False, 184 parse_styles=parse_styles, 185 style_delimiters=style_delimiters, 186 syntax_language=None, 187 syntax_theme=None, 188 ) 189 return box 190 191 def code( 192 self, 193 text: Sv[str], 194 language: Sn[str] = None, 195 style: Sn[TextStyle] = None, 196 *, 197 align: Sv[TextAlign] = "start", 198 strip: bool = True, 199 theme: Sn[str] = None, 200 parse_styles: bool = False, 201 style_delimiters: str = "~{}", 202 parse_steps: bool | str = False, 203 **box_args, 204 ): 205 if strip and isinstance(text, str): 206 text = text.strip() 207 if parse_steps: 208 text = parse_steps_helper(text, parse_steps, strip) 209 sv_check(text, check_is_str) 210 sv_check(align, check_text_align) 211 sn_check(style, check_is_str_or_text_style) 212 sn_check(language, check_is_str) 213 sn_check(theme, check_is_str) 214 box = self.box(**box_args) 215 box._content = TextContent( 216 text=text, 217 style=style, 218 align=align, 219 is_code=True, 220 syntax_language=language, 221 syntax_theme=theme, 222 parse_styles=parse_styles, 223 style_delimiters=style_delimiters, 224 ) 225 return box 226 227 def image( 228 self, 229 path_or_data: Sn[PathOrImageData], 230 *, 231 enable_steps: Sv[bool] = True, 232 shift_steps: int = 0, 233 watch=True, 234 **box_args, 235 ): 236 sn_check(path_or_data, check_image_path_or_data) 237 sv_check(enable_steps, check_is_bool) 238 sv_check(shift_steps, check_is_int) 239 if watch: 240 path_or_data = sn_map(path_or_data, normalize_and_watch_image_path) 241 else: 242 path_or_data = sn_map(path_or_data, normalize_image_path) 243 box = self.box(**box_args) 244 box._content = ImageContent(path_or_data, enable_steps, shift_steps) 245 return box 246 247 def set_style(self, name: str, style: Sn[TextStyle]): 248 if name == "default": 249 self.update_style(name, style) 250 return 251 check_is_str(name) 252 sn_check(style, check_is_text_style) 253 self._set_style(name, style) 254 255 def update_style(self, name: str, style: TextStyle): 256 check_is_str(name) 257 check_is_text_style(style) 258 old_style = self._get_style(name) 259 if old_style is None: 260 self._set_style(name, style) 261 elif isinstance(old_style, TextStyle): 262 self._set_style(name, old_style.merge(style)) 263 else: 264 raise Exception( 265 "Non-primitive style cannot be updated; use set_style instead" 266 ) 267 268 def x(self, width_fraction: IntOrFloat = 0) -> LayoutExpr: 269 """ 270 Get an expression with X coordinate relative to the box. 271 """ 272 check_is_int_or_float(width_fraction) 273 node_id = id(self) 274 expr = LayoutExpr.x(node_id) 275 if width_fraction == 0: 276 return expr 277 return expr + LayoutExpr.width(node_id, width_fraction) 278 279 def y(self, height_fraction: IntOrFloat = 0) -> LayoutExpr: 280 """ 281 Get an expression with Y coordinate relative to the box. 282 """ 283 check_is_int_or_float(height_fraction) 284 node_id = id(self) 285 expr = LayoutExpr.y(node_id) 286 if height_fraction == 0: 287 return expr 288 return expr + LayoutExpr.height(node_id, height_fraction) 289 290 def p(self, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 291 """ 292 Get an expression with X and Y padding relative to the box. 293 """ 294 return Point(self.x(x), self.y(y)) 295 296 def width(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 297 """ 298 Get an expression with width of the parent box. 299 """ 300 check_is_int_or_float(fraction) 301 node_id = id(self) 302 return LayoutExpr.width(node_id, fraction) 303 304 def height(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 305 """ 306 Get an expression with height of the parent box. 307 """ 308 check_is_int_or_float(fraction) 309 node_id = id(self) 310 return LayoutExpr.height(node_id, fraction) 311 312 def line_x(self, line_idx: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 313 """ 314 Get an expression with X coordinate of a given line of text in the box. 315 """ 316 check_is_int_or_float(width_fraction) 317 check_is_int(line_idx) 318 node_id = id(self) 319 expr = LayoutExpr.line_x(node_id, line_idx) 320 if width_fraction == 0: 321 return expr 322 return expr + LayoutExpr.line_width(node_id, line_idx, width_fraction) 323 324 def line_y(self, line_idx: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 325 """ 326 Get an expression with Y coordinate of a given line of text in the box. 327 """ 328 check_is_int_or_float(height_fraction) 329 check_is_int(line_idx) 330 node_id = id(self) 331 expr = LayoutExpr.line_y(node_id, line_idx) 332 if height_fraction == 0: 333 return expr 334 return expr + LayoutExpr.line_height(node_id, line_idx, height_fraction) 335 336 def line_p(self, line_idx: int, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 337 return Point(self.line_x(line_idx, x), self.line_y(line_idx, y)) 338 339 def line_width(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 340 """ 341 Get an expression with a width of a given line of text in the box. 342 """ 343 check_is_int_or_float(fraction) 344 check_is_int(line_idx) 345 node_id = id(self) 346 return LayoutExpr.line_width(node_id, line_idx, fraction) 347 348 def line_height(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 349 """ 350 Get an expression with a height of a given line of text in the box. 351 """ 352 check_is_int_or_float(fraction) 353 check_is_int(line_idx) 354 node_id = id(self) 355 return LayoutExpr.line_height(node_id, line_idx, fraction) 356 357 def inline_x(self, anchor_id: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 358 """ 359 Get an expression with X coordinate of a given text anchor in the box. 360 """ 361 check_is_int_or_float(width_fraction) 362 check_is_int(anchor_id) 363 node_id = id(self) 364 expr = LayoutExpr.inline_x(node_id, anchor_id) 365 if width_fraction == 0: 366 return expr 367 return expr + LayoutExpr.inline_width(node_id, anchor_id, width_fraction) 368 369 def inline_y(self, anchor_id: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 370 """ 371 Get an expression with Y coordinate of a given text anchor in the box. 372 """ 373 check_is_int_or_float(height_fraction) 374 check_is_int(anchor_id) 375 node_id = id(self) 376 expr = LayoutExpr.inline_y(node_id, anchor_id) 377 if height_fraction == 0: 378 return expr 379 return expr + LayoutExpr.inline_height(node_id, anchor_id, height_fraction) 380 381 def inline_p(self, anchor_id: int, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 382 return Point(self.inline_x(anchor_id, x), self.inline_y(anchor_id, y)) 383 384 def inline_width(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 385 """ 386 Get an expression with a height of a given text anchor in the box. 387 """ 388 check_is_int_or_float(fraction) 389 check_is_int(anchor_id) 390 node_id = id(self) 391 return LayoutExpr.inline_width(node_id, anchor_id, fraction) 392 393 def inline_height(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 394 """ 395 Get an expression with a height of a given text anchor in the box. 396 """ 397 check_is_int_or_float(fraction) 398 check_is_int(anchor_id) 399 node_id = id(self) 400 return LayoutExpr.inline_height(node_id, anchor_id, fraction) 401 402 def line_box(self, line_idx: int, n_lines: int = 1, **box_args): 403 """ 404 Creates a new box over a text line in the box 405 """ 406 height = self.line_height(line_idx) 407 if n_lines != 1: 408 check_is_int(line_idx) 409 height = height * n_lines 410 width = LayoutExpr.max( 411 [self.line_width(line_idx + i) for i in range(n_lines)] 412 ) 413 else: 414 width = self.line_width(line_idx) 415 return self.box( 416 x=self.line_x(line_idx), 417 y=self.line_y(line_idx), 418 width=width, 419 height=height, 420 **box_args, 421 ) 422 423 def inline_box(self, anchor_id: int, **box_args): 424 """ 425 Creates a new box over a inline text anchor in the box 426 """ 427 428 return self.box( 429 x=self.inline_x(anchor_id), 430 y=self.inline_y(anchor_id), 431 width=self.inline_width(anchor_id), 432 height=self.inline_height(anchor_id), 433 **box_args, 434 )
61 def add(self, box: Union[Path, Rect, Oval, "Box"]): 62 """ 63 Adds Box or a geometry item into the box 64 """ 65 raise NotImplementedError
Adds Box or a geometry item into the box
73 def box( 74 self, 75 *, 76 x: Sn[Position] = None, 77 y: Sn[Position] = None, 78 z_level: Sn[int] = None, 79 show: BoolStepDef = True, 80 active: BoolStepDef = True, 81 width: Sn[Size] = None, 82 height: Sn[Size] = None, 83 bg_color: Sn[str] = None, 84 row: Sv[bool] = False, 85 reverse: Sv[bool] = False, 86 p_left: Sv[Length] = 0, 87 p_right: Sv[Length] = 0, 88 p_top: Sv[Length] = 0, 89 p_bottom: Sv[Length] = 0, 90 m_left: Sv[LengthAuto] = 0, 91 m_right: Sv[LengthAuto] = 0, 92 m_top: Sv[LengthAuto] = 0, 93 m_bottom: Sv[LengthAuto] = 0, 94 flex_grow: Sv[float] = 0.0, 95 flex_shrink: Sv[float] = 1.0, 96 align_items: Sn[AlignItems] = None, 97 align_self: Sn[AlignItems] = None, 98 justify_self: Sn[AlignItems] = None, 99 align_content: Sn[AlignContent] = None, 100 justify_content: Sn[AlignContent] = None, 101 gap_x: Sv[Length] = 0, 102 gap_y: Sv[Length] = 0, 103 grid: Sn[GridOptions] = None, 104 border_radius: Sv[IntOrFloat] = 0, 105 url: Sn[str] = None, 106 name: str = "", 107 debug_layout: bool | str | None = None, 108 ): 109 """ 110 Create a new child box. See [Box reference](https://spirali.github.io/nelsie/guide/box/) for documentation 111 """ 112 box = Box( 113 x=x, 114 y=y, 115 z_level=z_level, 116 show=show, 117 active=active, 118 width=width, 119 height=height, 120 bg_color=bg_color, 121 row=row, 122 reverse=reverse, 123 p_left=p_left, 124 p_right=p_right, 125 p_top=p_top, 126 p_bottom=p_bottom, 127 m_left=m_left, 128 m_right=m_right, 129 m_top=m_top, 130 m_bottom=m_bottom, 131 flex_grow=flex_grow, 132 flex_shrink=flex_shrink, 133 align_items=align_items, 134 align_self=align_self, 135 justify_self=justify_self, 136 align_content=align_content, 137 justify_content=justify_content, 138 gap_x=gap_x, 139 gap_y=gap_y, 140 grid=grid, 141 name=name, 142 border_radius=border_radius, 143 debug_layout=debug_layout, 144 url=url, 145 ) 146 self.add(box) 147 return box
Create a new child box. See Box reference for documentation
149 def overlay(self, **box_args): 150 """ 151 Create a new box that spans over the box 152 """ 153 box_args.setdefault("x", 0) 154 box_args.setdefault("y", 0) 155 box_args.setdefault("width", "100%") 156 box_args.setdefault("height", "100%") 157 return self.box(**box_args)
Create a new box that spans over the box
159 def text( 160 self, 161 text: Sv[str], 162 style: Sn[TextStyle] = None, 163 *, 164 align: Sv[TextAlign] = "start", 165 strip: bool = True, 166 parse_styles: bool = True, 167 style_delimiters: str = "~{}", 168 parse_steps: bool | str = False, 169 **box_args, 170 ): 171 if strip and isinstance(text, str): 172 text = text.strip() 173 if parse_steps: 174 text = parse_steps_helper(text, parse_steps, strip) 175 sv_check(text, check_is_str) 176 sv_check(align, check_text_align) 177 sn_check(style, check_is_str_or_text_style) 178 box = self.box(**box_args) 179 box._content = TextContent( 180 text=text, 181 style=style, 182 align=align, 183 is_code=False, 184 parse_styles=parse_styles, 185 style_delimiters=style_delimiters, 186 syntax_language=None, 187 syntax_theme=None, 188 ) 189 return box
191 def code( 192 self, 193 text: Sv[str], 194 language: Sn[str] = None, 195 style: Sn[TextStyle] = None, 196 *, 197 align: Sv[TextAlign] = "start", 198 strip: bool = True, 199 theme: Sn[str] = None, 200 parse_styles: bool = False, 201 style_delimiters: str = "~{}", 202 parse_steps: bool | str = False, 203 **box_args, 204 ): 205 if strip and isinstance(text, str): 206 text = text.strip() 207 if parse_steps: 208 text = parse_steps_helper(text, parse_steps, strip) 209 sv_check(text, check_is_str) 210 sv_check(align, check_text_align) 211 sn_check(style, check_is_str_or_text_style) 212 sn_check(language, check_is_str) 213 sn_check(theme, check_is_str) 214 box = self.box(**box_args) 215 box._content = TextContent( 216 text=text, 217 style=style, 218 align=align, 219 is_code=True, 220 syntax_language=language, 221 syntax_theme=theme, 222 parse_styles=parse_styles, 223 style_delimiters=style_delimiters, 224 ) 225 return box
227 def image( 228 self, 229 path_or_data: Sn[PathOrImageData], 230 *, 231 enable_steps: Sv[bool] = True, 232 shift_steps: int = 0, 233 watch=True, 234 **box_args, 235 ): 236 sn_check(path_or_data, check_image_path_or_data) 237 sv_check(enable_steps, check_is_bool) 238 sv_check(shift_steps, check_is_int) 239 if watch: 240 path_or_data = sn_map(path_or_data, normalize_and_watch_image_path) 241 else: 242 path_or_data = sn_map(path_or_data, normalize_image_path) 243 box = self.box(**box_args) 244 box._content = ImageContent(path_or_data, enable_steps, shift_steps) 245 return box
255 def update_style(self, name: str, style: TextStyle): 256 check_is_str(name) 257 check_is_text_style(style) 258 old_style = self._get_style(name) 259 if old_style is None: 260 self._set_style(name, style) 261 elif isinstance(old_style, TextStyle): 262 self._set_style(name, old_style.merge(style)) 263 else: 264 raise Exception( 265 "Non-primitive style cannot be updated; use set_style instead" 266 )
268 def x(self, width_fraction: IntOrFloat = 0) -> LayoutExpr: 269 """ 270 Get an expression with X coordinate relative to the box. 271 """ 272 check_is_int_or_float(width_fraction) 273 node_id = id(self) 274 expr = LayoutExpr.x(node_id) 275 if width_fraction == 0: 276 return expr 277 return expr + LayoutExpr.width(node_id, width_fraction)
Get an expression with X coordinate relative to the box.
279 def y(self, height_fraction: IntOrFloat = 0) -> LayoutExpr: 280 """ 281 Get an expression with Y coordinate relative to the box. 282 """ 283 check_is_int_or_float(height_fraction) 284 node_id = id(self) 285 expr = LayoutExpr.y(node_id) 286 if height_fraction == 0: 287 return expr 288 return expr + LayoutExpr.height(node_id, height_fraction)
Get an expression with Y coordinate relative to the box.
290 def p(self, x: IntOrFloat = 0, y: IntOrFloat = 0) -> Point: 291 """ 292 Get an expression with X and Y padding relative to the box. 293 """ 294 return Point(self.x(x), self.y(y))
Get an expression with X and Y padding relative to the box.
296 def width(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 297 """ 298 Get an expression with width of the parent box. 299 """ 300 check_is_int_or_float(fraction) 301 node_id = id(self) 302 return LayoutExpr.width(node_id, fraction)
Get an expression with width of the parent box.
304 def height(self, fraction: IntOrFloat = 1.0) -> LayoutExpr: 305 """ 306 Get an expression with height of the parent box. 307 """ 308 check_is_int_or_float(fraction) 309 node_id = id(self) 310 return LayoutExpr.height(node_id, fraction)
Get an expression with height of the parent box.
312 def line_x(self, line_idx: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 313 """ 314 Get an expression with X coordinate of a given line of text in the box. 315 """ 316 check_is_int_or_float(width_fraction) 317 check_is_int(line_idx) 318 node_id = id(self) 319 expr = LayoutExpr.line_x(node_id, line_idx) 320 if width_fraction == 0: 321 return expr 322 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.
324 def line_y(self, line_idx: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 325 """ 326 Get an expression with Y coordinate of a given line of text in the box. 327 """ 328 check_is_int_or_float(height_fraction) 329 check_is_int(line_idx) 330 node_id = id(self) 331 expr = LayoutExpr.line_y(node_id, line_idx) 332 if height_fraction == 0: 333 return expr 334 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.
339 def line_width(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 340 """ 341 Get an expression with a width of a given line of text in the box. 342 """ 343 check_is_int_or_float(fraction) 344 check_is_int(line_idx) 345 node_id = id(self) 346 return LayoutExpr.line_width(node_id, line_idx, fraction)
Get an expression with a width of a given line of text in the box.
348 def line_height(self, line_idx: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 349 """ 350 Get an expression with a height of a given line of text in the box. 351 """ 352 check_is_int_or_float(fraction) 353 check_is_int(line_idx) 354 node_id = id(self) 355 return LayoutExpr.line_height(node_id, line_idx, fraction)
Get an expression with a height of a given line of text in the box.
357 def inline_x(self, anchor_id: int, width_fraction: IntOrFloat = 0) -> LayoutExpr: 358 """ 359 Get an expression with X coordinate of a given text anchor in the box. 360 """ 361 check_is_int_or_float(width_fraction) 362 check_is_int(anchor_id) 363 node_id = id(self) 364 expr = LayoutExpr.inline_x(node_id, anchor_id) 365 if width_fraction == 0: 366 return expr 367 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.
369 def inline_y(self, anchor_id: int, height_fraction: IntOrFloat = 0) -> LayoutExpr: 370 """ 371 Get an expression with Y coordinate of a given text anchor in the box. 372 """ 373 check_is_int_or_float(height_fraction) 374 check_is_int(anchor_id) 375 node_id = id(self) 376 expr = LayoutExpr.inline_y(node_id, anchor_id) 377 if height_fraction == 0: 378 return expr 379 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.
384 def inline_width(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 385 """ 386 Get an expression with a height of a given text anchor in the box. 387 """ 388 check_is_int_or_float(fraction) 389 check_is_int(anchor_id) 390 node_id = id(self) 391 return LayoutExpr.inline_width(node_id, anchor_id, fraction)
Get an expression with a height of a given text anchor in the box.
393 def inline_height(self, anchor_id: int, fraction: IntOrFloat = 1.0) -> LayoutExpr: 394 """ 395 Get an expression with a height of a given text anchor in the box. 396 """ 397 check_is_int_or_float(fraction) 398 check_is_int(anchor_id) 399 node_id = id(self) 400 return LayoutExpr.inline_height(node_id, anchor_id, fraction)
Get an expression with a height of a given text anchor in the box.
402 def line_box(self, line_idx: int, n_lines: int = 1, **box_args): 403 """ 404 Creates a new box over a text line in the box 405 """ 406 height = self.line_height(line_idx) 407 if n_lines != 1: 408 check_is_int(line_idx) 409 height = height * n_lines 410 width = LayoutExpr.max( 411 [self.line_width(line_idx + i) for i in range(n_lines)] 412 ) 413 else: 414 width = self.line_width(line_idx) 415 return self.box( 416 x=self.line_x(line_idx), 417 y=self.line_y(line_idx), 418 width=width, 419 height=height, 420 **box_args, 421 )
Creates a new box over a text line in the box
423 def inline_box(self, anchor_id: int, **box_args): 424 """ 425 Creates a new box over a inline text anchor in the box 426 """ 427 428 return self.box( 429 x=self.inline_x(anchor_id), 430 y=self.inline_y(anchor_id), 431 width=self.inline_width(anchor_id), 432 height=self.inline_height(anchor_id), 433 **box_args, 434 )
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.
52@dataclass 53class GridOptions: 54 template_rows: Sv[GridTemplate] = () 55 template_columns: Sv[GridTemplate] = () 56 row: Sn[GridPosition] = None 57 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()