• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

manahl / PyBloqs / e0ef5b11-a828-46c3-b0cf-c452bea7e733

18 Sep 2024 08:17PM UTC coverage: 87.335% (-0.04%) from 87.377%
e0ef5b11-a828-46c3-b0cf-c452bea7e733

Pull #112

circleci

rspencer01
Add pytest
Pull Request #112: Upgrade code from python 2

99 of 117 new or added lines in 20 files covered. (84.62%)

17 existing lines in 2 files now uncovered.

2117 of 2424 relevant lines covered (87.33%)

2.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.75
/pybloqs/block/base.py
1
import builtins
3✔
2
import contextlib
3✔
3
import os
3✔
4
import uuid
3✔
5
import webbrowser
3✔
6
from io import BytesIO
3✔
7
from urllib.parse import urljoin
3✔
8

9
import pybloqs.htmlconv as htmlconv
3✔
10
from pybloqs.config import user_config
3✔
11
from pybloqs.email import send_html_report
3✔
12
from pybloqs.html import append_to, id_generator, js_elem, render, root
3✔
13
from pybloqs.static import Css, DependencyTracker, register_interactive, script_block_core, script_inflate
3✔
14
from pybloqs.util import Cfg, cfg_to_css_string
3✔
15

16
default_css_main = Css(os.path.join("css", "pybloqs_default", "main"))
3✔
17
register_interactive(default_css_main)
3✔
18

19

20
class BaseBlock:
3✔
21
    """
22
    Base class for all blocks. Provides infrastructure for rendering the block
23
    in an IPython Notebook or saving it to disk in HTML, PDF, PNG or JPG format.
24
    """
25

26
    container_tag = "div"
3✔
27
    resource_deps = ()
3✔
28

29
    def __init__(
3✔
30
        self,
31
        title=None,
32
        title_level=3,
33
        title_wrap=False,
34
        width=None,
35
        height=None,
36
        inherit_cfg=True,
37
        styles=None,
38
        classes=(),
39
        anchor=None,
40
        **kwargs,
41
    ):
42
        self._settings = Cfg(
3✔
43
            title=title,
44
            title_level=title_level,
45
            title_wrap=title_wrap,
46
            cascading_cfg=Cfg(**kwargs).override(styles or Cfg()),
47
            default_cfg=Cfg(),
48
            inherit_cfg=inherit_cfg,
49
            width=width,
50
            height=height,
51
            classes=["pybloqs"] + ([classes] if isinstance(classes, str) else list(classes)),
52
        )
53
        # Anchor should not be inherited, so keep outside of Cfg
54
        self._anchor = anchor
3✔
55
        self._id = uuid.uuid4().hex
3✔
56

57
    def render_html(self, pretty=True, static_output=False, header_block=None, footer_block=None):
3✔
58
        """Returns html output of the block
59
        :param pretty: Toggles pretty printing of the resulting HTML. Not applicable for non-HTML output.
60
        :param static_output: Passed down to _write_block. Will render static version of blocks which support this.
61
        :param header_block: If not None, header is inlined into a HTML body as table.
62
        :param footer_block: If not None, header is inlined into a HTML body as table.
63
        :return html-code of the block
64
        """
65
        # Render the contents
66
        html = root("html", doctype="html")
3✔
67
        head = append_to(html, "head")
3✔
68
        append_to(head, "meta", charset="utf-8")
3✔
69

70
        body = append_to(html, "body")
3✔
71

72
        # Make sure that the main style sheet is always included
73
        resource_deps = DependencyTracker(default_css_main)
3✔
74

75
        # If header or footer are passed into this function, inline them in the following structure:
76
        #
77
        # <body>
78
        # <table>
79
        #    <thead><tr><td>Header html</td></tr></thead>
80
        #    <tfoot><tr><td>Footer html</td></tr></tfoot>
81
        #    <tbody><tr><td>Body html</td></tr></tbody>
82
        # </table>
83
        # </body>
84
        if header_block is not None or footer_block is not None:
3✔
85
            content_table = append_to(body, "table")
3✔
86
            if header_block is not None:
3✔
87
                header_thead = append_to(content_table, "thead")
3✔
88
                header_tr = append_to(header_thead, "tr")
3✔
89
                header_td = append_to(header_tr, "th")
3✔
90
                header_block._write_block(
3✔
91
                    header_td, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output
92
                )
93

94
            if footer_block is not None:
3✔
95
                footer_tfoot = append_to(content_table, "tfoot", id="footer")
3✔
96
                footer_tr = append_to(footer_tfoot, "tr")
3✔
97
                footer_td = append_to(footer_tr, "td")
3✔
98
                footer_block._write_block(
3✔
99
                    footer_td, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output
100
                )
101

102
            body_tbody = append_to(content_table, "tbody")
3✔
103
            body_tr = append_to(body_tbody, "tr")
3✔
104
            body_td = append_to(body_tr, "td")
3✔
105
            self._write_block(body_td, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output)
3✔
106
        else:
107
            self._write_block(body, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output)
3✔
108

109
        script_inflate.write(head)
3✔
110
        script_block_core.write(head)
3✔
111

112
        if static_output:
3✔
113
            # Add the load wait poller if there are any JS resources
114
            js_elem(body, "var loadWaitPoller=runWaitPoller();")
3✔
115

116
        # Write out resources
117
        for res in resource_deps:
3✔
118
            res.write(head)
3✔
119

120
        # Render the whole document (the parent of the html tag)
121
        content = render(html.parent, pretty=pretty)
3✔
122
        return content
3✔
123

124
    def save(
3✔
125
        self,
126
        filename=None,
127
        fmt=None,
128
        pdf_zoom=1,
129
        pdf_page_size=htmlconv.html_converter.A4,
130
        pdf_auto_shrink=True,
131
        orientation=htmlconv.html_converter.PORTRAIT,
132
        header_block=None,
133
        header_spacing=5,
134
        footer_block=None,
135
        footer_spacing=5,
136
        **kwargs,
137
    ):
138
        """
139
        Render and save the block. Depending on whether the filename or the format is
140
        provided, the content will either be written out to a file or returned as a string.
141

142
        :param filename: Format will be based on the file extension.
143
                         The following formats are supported:
144
                         - HTML
145
                         - PDF
146
                         - PNG
147
                         - JPG
148
        :param fmt: Specifies the format of a temporary output file. When supplied, the filename
149
                    parameter must be omitted.
150
        :param pdf_zoom: The zooming to apply when rendering the page.
151
        :param pdf_page_size: The page size to use when rendering the page to PDF.
152
        :param pdf_auto_shrink: Toggles auto-shrinking content to fit the desired page size (wkhtmltopdf only)
153
        :param orientation: Either html_converter.PORTRAIT or html_converter.LANDSCAPE
154
        :param header_block: Block to be used as header (and repeated on every page). Only used for PDF output.
155
        :param header_spacing: Size of header block. Numbers are in mm. HTML sizes (e.g. '5cm') in chrome_headless only.
156
        :param footer_block: Block to be used as footer (and repeated on every page). Only used for PDF output.
157
        :param footer_spacing: Size of header block. Numbers are in mm. HTML sizes (e.g. '5cm') in chrome_headless only.
158
        :return: html filename
159
        """
160
        # Ensure that exactly one of filename or fmt is provided
161
        if filename is None and fmt is None:
3✔
162
            raise ValueError("One of `filename` or `fmt` must be provided.")
3✔
163

164
        tempdir = user_config["tmp_html_dir"]
3✔
165

166
        if filename:
3✔
167
            _, fmt_from_name = os.path.splitext(filename)
3✔
168
            # Exclude the dot from the extension, gosh darn it!
169
            fmt_from_name = fmt_from_name[1:]
3✔
170
            if fmt is None:
3✔
171
                if fmt_from_name == "":
3✔
172
                    raise ValueError("If fmt is not specified, filename must contain extension")
3✔
173
                fmt = fmt_from_name
3✔
174
            else:
175
                if fmt != fmt_from_name:
3✔
176
                    filename += "." + fmt
3✔
177
        else:
178
            name = self._id[: user_config["id_precision"]] + "." + fmt
3✔
179
            filename = os.path.join(tempdir, name)
3✔
180

181
        # Force extension to be lower case so format checks are easier later
182
        fmt = fmt.lower()
3✔
183

184
        is_html = "htm" in fmt
3✔
185

186
        if is_html:
3✔
187
            content = self.render_html(static_output=False, header_block=header_block, footer_block=footer_block)
3✔
188
            with builtins.open(filename, "w", encoding="utf-8") as f:
3✔
189
                f.write(content)
3✔
190
        else:
191
            converter = htmlconv.get_converter(fmt)
3✔
192
            converter.htmlconv(
3✔
193
                self,
194
                filename,
195
                header_block=header_block,
196
                header_spacing=header_spacing,
197
                footer_block=footer_block,
198
                footer_spacing=footer_spacing,
199
                pdf_page_size=pdf_page_size,
200
                orientation=orientation,
201
                pdf_auto_shrink=pdf_auto_shrink,
202
                pdf_zoom=pdf_zoom,
203
                **kwargs,
204
            )
205
        return filename
3✔
206

207
    def publish(self, name, *args, **kwargs):
3✔
208
        """
209
        Publish the block so that others can access it.
210

211
        :param name: Name to publish under. Can be a filename or a relative path.
212
        :param args: Arguments to pass to `Block.save`.
213
        :param kwargs: Keyword arguments to pass to `Block.save`.
214
        :return: Path to the published block file.
215
        """
216
        full_path = os.path.join(user_config["public_dir"], name)
3✔
217
        full_path = os.path.expanduser(full_path)
3✔
218

219
        base_dir = os.path.dirname(full_path)
3✔
220

221
        with contextlib.suppress(OSError):  # Ignore if directory already exists
3✔
222
            os.makedirs(base_dir)
3✔
223

224
        self.save(full_path, *args, **kwargs)
3✔
225

226
        return full_path
3✔
227

228
    def show(self, fmt="html", header_block=None, footer_block=None):
3✔
229
        """
230
        Show the block in a browser.
231

232
        :param fmt: The format of the saved block. Supports the same output as `Block.save`
233
        :return: Path to the block file.
234
        """
235
        file_name = self._id[: user_config["id_precision"]] + "." + fmt
3✔
236
        file_path = self.publish(
3✔
237
            os.path.expanduser(os.path.join(user_config["tmp_html_dir"], file_name)),
238
            header_block=header_block,
239
            footer_block=footer_block,
240
        )
241

242
        try:
3✔
243
            url_base = user_config["public_dir"]
3✔
244
        except KeyError:
3✔
245
            path = os.path.expanduser(file_path)
3✔
246
        else:
247
            path = urljoin(url_base, os.path.expanduser(user_config["tmp_html_dir"] + "/" + file_name))
3✔
248

249
        webbrowser.open_new_tab(path)
3✔
250

251
        return path
3✔
252

253
    def email(
3✔
254
        self,
255
        title="",
256
        recipients=(user_config["user_email_address"],),
257
        header_block=None,
258
        footer_block=None,
259
        from_address=None,
260
        cc=None,
261
        bcc=None,
262
        attachments=None,
263
        convert_to_ascii=True,
264
        **kwargs,
265
    ):
266
        """
267
        Send the rendered blocks as email. Each output format chosen will be added as an
268
        attachment.
269

270
        :param title: title of the email
271
        :param recipients: recipient of the email
272
        :param fmt: One or more output formats that should be included as attachments.
273
                    The following formats are supported:
274
                    - HTML
275
                    - PDF
276
                    - PNG
277
                    - JPG
278
        :param body_block: The block to use as the email body. The default behavior is
279
                          to use the current block.
280
        :param from_address: sender of the message. Defaults to user name.
281
            Can be overwritten in .pybloqs.cfg with yaml format: 'user_email_address: a@b.com'
282
        :param cc: cc recipient
283
        :param bcc: bcc recipient
284
        :param convert_to_ascii: bool to control convertion of html email to ascii or to leave in current format
285
        :param kwargs: Optional arguments to pass to `Block.render_html()`
286
        """
287
        if from_address is None:
×
UNCOV
288
            from_address = user_config["user_email_address"]
×
289

290
        # The email body needs to be static without any dynamic elements.
UNCOV
291
        email_html = self.render_html(header_block=header_block, footer_block=footer_block, **kwargs)
×
292

UNCOV
293
        send_html_report(
×
294
            email_html,
295
            recipients,
296
            subject=title,
297
            attachments=attachments,
298
            From=from_address,
299
            Cc=cc,
300
            Bcc=bcc,
301
            convert_to_ascii=convert_to_ascii,
302
        )
303

304
    def to_static(self):
3✔
UNCOV
305
        return self._visit(lambda block: block._to_static())
×
306

307
    def _to_static(self):
3✔
308
        """
309
        Subclasses can override this method to provide a static content version.
310
        """
UNCOV
311
        return self
×
312

313
    def _visit(self, visitor):
3✔
314
        """
315
        Calls the supplied visitor function on this block and any sub-blocks
316
        :param visitor: Visitor function
317
        :return: Return value of the visitor
318
        """
UNCOV
319
        return visitor(self)
×
320

321
    def _provide_default_cfg(self, defaults):
3✔
322
        """
323
        Makes the supplied config to be part of the defaults for the block.
324
        :param defaults: The default parameters that should be inherited.
325
        """
UNCOV
326
        self._settings.default_cfg = self._settings.default_cfg.inherit(defaults)
×
327

328
    def _combine_parent_cfg(self, parent_cfg):
3✔
329
        """from pybloqs.config import user_config
330
        Combine the supplied parent and the current Block's config.
331

332
        :param parent_cfg: Parent config to inherit from.
333
        :return: Combined config.
334
        """
335
        # Combine parameters only if inheritance is turned on
336
        if self._settings.inherit_cfg:
3✔
337
            actual_cfg = self._settings.cascading_cfg.inherit(parent_cfg)
3✔
338
        else:
UNCOV
339
            actual_cfg = self._settings.cascading_cfg
×
340

341
        # Any undefined settings will use the defaults
342
        actual_cfg = actual_cfg.inherit(self._settings.default_cfg)
3✔
343

344
        return actual_cfg
3✔
345

346
    def _get_styles_string(self, styles_cfg):
3✔
347
        """
348
        Converts the styles configuration to a CSS styles string.
349

350
        :param styles_cfg: The configuration object to convert.
351
        :return: CSS string
352
        """
353
        sizing_cfg = Cfg()
3✔
354

355
        if self._settings.width is not None:
3✔
356
            sizing_cfg["width"] = self._settings.width
3✔
357

358
        if self._settings.height is not None:
3✔
359
            sizing_cfg["height"] = self._settings.height
3✔
360

361
        # Replace `_` with `-` and make values lowercase to get valid CSS names
362
        return cfg_to_css_string(styles_cfg.override(sizing_cfg))
3✔
363

364
    def _write_block(self, parent, parent_cfg, id_gen, resource_deps=None, static_output=False):
3✔
365
        """
366
        Writes out the block into the supplied stream, inheriting the parent_parameters.
367

368
        :param parent: Parent element
369
        :param parent_cfg: Parent parameters to inherit.
370
        :param id_gen: Unique ID generator.
371
        :param resource_deps: Object used to register resource dependencies.
372
        :param static_output: A value of True signals to blocks that the final output will
373
                              be a static format. Certain dynamic content will render with
374
                              alternate options.
375

376
        """
377
        if resource_deps is not None:
3✔
378
            for res in self.resource_deps:
3✔
379
                resource_deps.add(res)
3✔
380

381
        actual_cfg = self._combine_parent_cfg(parent_cfg)
3✔
382

383
        if self.container_tag is not None:
3✔
384
            container = append_to(parent, self.container_tag)
3✔
385
            self._write_container_attrs(container, actual_cfg)
3✔
386
        else:
UNCOV
387
            container = parent
×
388

389
        self._write_anchor(container)
3✔
390
        self._write_title(container)
3✔
391
        self._write_contents(container, actual_cfg, id_gen, resource_deps=resource_deps, static_output=static_output)
3✔
392

393
    def _write_container_attrs(self, container, actual_cfg):
3✔
394
        """
395
        Writes out the container attributes (styles, class, etc...).
396
        Note that this method will only be called if the container tag is not `None`.
397

398
        :param container: Container element.
399
        :param actual_cfg: Actual parameters to use.
400
        """
401
        styles = self._get_styles_string(actual_cfg)
3✔
402
        if len(styles) > 0:
3✔
403
            container["style"] = styles
3✔
404

405
        container["class"] = self._settings.classes
3✔
406

407
    def _write_title(self, container):
3✔
408
        """
409
        Write out the title (if there is any).
410

411
        :param container: Container element.
412
        """
413
        if self._settings.title is not None and (self._settings.title != ""):
3✔
414
            title = append_to(
3✔
415
                container,
416
                f"H{self._settings.title_level}",
417
                style="white-space: %s" % ("normal" if self._settings.title_wrap else "nowrap"),
418
            )
419
            title.string = self._settings.title
3✔
420

421
    def _write_anchor(self, container):
3✔
422
        """
423
        Write HTML anchor for linking within page
424

425
        :param container: Container element.
426
        """
427
        if self._anchor is not None:
3✔
428
            append_to(container, "a", name=self._anchor)
3✔
429

430
    def _write_contents(self, container, actual_cfg, id_gen, resource_deps=None, static_output=None):
3✔
431
        """
432
        Write out the actual contents of the block. Deriving classes must override
433
        this method.
434

435
        :param container: Container element.
436
        :param actual_cfg: Actual parameters to use.
437
        :param id_gen: Unique ID generator.
438
        :param resource_deps: Object used to register resource dependencies.
439
        :param static_output: A value of True signals to blocks that the final output will
440
                              be a static format. Certain dynamic content will render with
441
                              alternate options.
442
        """
UNCOV
443
        raise NotImplementedError("_write_contents")
×
444

445
    def _repr_html_(self, *_):
3✔
446
        """
447
        Function required to support interactive IPython plopping and plotting.
448

449
        Should not be used directly.
450

451
        :return: Data to be displayed
452
        """
UNCOV
453
        return self.data.decode()
×
454

455
    @property
3✔
456
    def data(self):
3✔
457
        """
458
        Function required to support interactive IPython plotting.
459

460
        Should not be used directly.
461

462
        :return: Data to be displayed
463
        """
464
        container = root("div")
×
UNCOV
465
        self._write_block(container, Cfg(), id_generator())
×
466

467
        # Write children into the output
UNCOV
468
        output = BytesIO()
×
469

470
        for child in container.children:
×
UNCOV
471
            output.write(render(child).encode("utf-8"))
×
472

UNCOV
473
        return output.getvalue()
×
474

475

476
class HRule(BaseBlock):
3✔
477
    """
478
    Draws a horizontal divider line.
479
    """
480

481
    def _write_block(self, parent, *args, **kwargs):
3✔
482
        # Add a `hr` element to the parent
483
        append_to(parent, "hr")
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc