Skip to content

Add text output to the Express drawing API - #112

Merged
wbkboyer merged 7 commits into
graph-algorithms:masterfrom
wheevu:fix/94-text-drawing-on-master
Oct 21, 2026
Merged

wbkboyer merged 7 commits into
graph-algorithms:masterfrom
wheevu:fix/94-text-drawing-on-master

Conversation

@wheevu

@wheevu wheevu commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Resolves #94

Adds output_type='text/plain' to both draw() APIs.
Text is returned as a string or written to a file.
Images stay the default, and ascii() still works.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • This change requires a documentation update

Changes

Added

  • tests/test_express_api_draw_text.py covers text output, files, errors, and compatibility.
  • tests/test_express_api_draw_documentation.py keeps private helpers out of the API docs.

Updated

  • Drawing APIs, README, and examples support text output.
  • ascii.py is renamed to draw_as_text.py.

Testing

  • pytest -vv: 118 passed on Python 3.11 and 3.14, macOS ARM64.
  • Examples, docs, and 12 compatibility checks pass.

Linux, Windows, Python 3.12, and Python 3.13 were not tested locally.

@wbkboyer
wbkboyer self-requested a review October 15, 2026 23:12
* Removed `cimport cython` since `@cython.embedsignature(False)` directive overrides are not necessary to exclude the "private" methods from the generated docs
    * __**NOTE:**__ I had previously been worried that including `cimport cython` and using the `@cython.embedsignature(False)` directive would cause an `ImportError` when a consuming Python application is run in an environment without Cython installed, but local testing shows this doesn't appear to be the case. This is because when we `pip install`, the isolated build environment has `cython` installed as build dependency due to `requires` in `pyproject.toml`, and so the extensions get cythonized, so Cython isn't a runtime dependency.
* Restored `PGraph.ascii()` method and `ascii()` function docstrings with deprecation warning
* Removed hash-led copy of `PGraph.ascii()` docstring from `PGraph._draw_as_text()` with a note to copy the docstring over when we remove `PGraph.ascii()` method
* `PGraph.ascii()` now calls `PGraph.draw()` with `label_type='index'` and `output_type='text/plain'` rather than directly calling `PGraph._draw_as_text()`
* Reordered keyword arguments in calls to `PGraph.draw()` method and `draw()` function to match their function signatures
* Moved `PGraph._draw_as_text()` to reside between `PGraph.draw()` and `PGraph._draw_as_image()`
* Made sure to add cross-references to various methods and functions in docstrings
@wbkboyer

Copy link
Copy Markdown
Member

Hey @wheevu ! Thank you for your contribution; after merging in master, I made a few changes to the original #94 spec and tweaked a few things (see commit message for 8a034c).

I'm going to have to pick up this review on Monday just to make sure I understand the added tests, because in particular I'm a bit confused by the check for the outfileName that you're doing in PGraph._draw_as_text():

        if outfileName is not None and '\0' in outfileName:
            raise ValueError("planarity: outfileName must not contain a NUL.")

Can you verify that you're trying to ensure that if an outfileName is given, that it is a valid str rather than a null-terminated char *, because otherwise the line encoded_filename = outfileName.encode('utf-8') will fail?

Thank you for your patience, and again, thank you for tackling this work! 🎉

@wheevu

wheevu commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Hi @wbkboyer! Thank you for the warm welcome! 😄

The check prevents filename truncation in the C layer.
outfileName is already declared as a Python str, and .encode('utf-8') preserves embedded NUL characters.

Without the check, 'notes.txt\0.bak' would reach gp_DrawPlanar_RenderToFile(), where fopen() would interpret it as 'notes.txt' and could overwrite that file. Raising ValueError should prevent this and would match Python's open() behavior.

test_text_rejects_embedded_nul_filename checks that the call raises and the existing file stays unchanged.

Hope that clears things up a bit. Thanks again for your time!

… indicate `ascii()` method and function are deprecated (but then filter those warnings in `pytest` using `pyproject.toml` `tool.pytest` section)

When indicating a function is deprecated, use `version-deprecated` rather than `deprecated` because as of version 9.0 of sphinx, `deprecated` was deprecated and maintained as an alias for backwards compatibility (lol).
@wbkboyer

wbkboyer commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Hi @wbkboyer! Thank you for the warm welcome! 😄

The check prevents filename truncation in the C layer. outfileName is already declared as a Python str, and .encode('utf-8') preserves embedded NUL characters.

Without the check, 'notes.txt\0.bak' would reach gp_DrawPlanar_RenderToFile(), where fopen() would interpret it as 'notes.txt' and could overwrite that file. Raising ValueError should prevent this and would match Python's open() behavior.

test_text_rejects_embedded_nul_filename checks that the call raises and the existing file stays unchanged.

Hope that clears things up a bit. Thanks again for your time!

Oh right the heck on, that makes sense that since both \ and 0 are valid characters, that some Python string containing \0 (which is considered as a single char according to len()) might be sent in as the outfileName, which would then get truncated at the C-layer! I didn't realize that passing in a string containing \0 would be accepted by Python and that it would be treated as just a normal character:

>>> foo = "hello\0world"
>>> print(foo)
helloworld
>>> len(foo)
11
>>> foo
'hello\x00world'

(For my future reference) Python Null Character (\0) in Strings contains a more in-depth discussion of the \0 char.

I think there's a couple other places this same safeguard (e.g. write() method within PGraph, as well as members of the full API), but that can be the subject of a future issue.

I've made a couple other fixes to docstrings in efae7c that I overlooked in 8a034c(in spite of my process of iteratively rebuilding the docs locally any time I changed the sourcefiles :S D'OH), and added DeprecationWarning-type warnings.warn() to the ascii() method and function (which I also made sure that pytest ignores with the added config in pyproject.toml).

I still want to double-check the tests; thanks for your patience with this process! 🎉 <3

…d some docstrings to tests to clarify underlying assumptions of the tests.

@wbkboyer wbkboyer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you again for your patience with this process, @wheevu ! I hope you can forgive me for all the suggested test name changes; I was trying to make sure they're consistent, and that they indicate precisely which path they're hitting, but please let me know what you think (e.g. more concise, consistent, and descriptive alternatives) 🎉

Also, if you think there's any subtleties that are hard to succinctly capture in the test names, may you please add short docstring that describes the various parameterizations or any assumptions that might be baked into the test?

Comment thread tests/test_express_api_draw_text.py Outdated
Comment thread tests/test_express_api_draw_text.py Outdated
Comment thread tests/test_express_api_draw_text.py
Comment thread tests/test_express_api_draw_text.py
Comment thread tests/test_express_api_draw_text.py Outdated
Comment thread tests/test_express_api_draw_text.py Outdated
Comment thread planarity/express/planarity.pyx Outdated
Comment thread tests/test_express_api_draw_text.py
Comment thread tests/test_express_api_draw_text.py
Comment thread tests/test_express_api_draw_text.py Outdated
@john-boyer-phd

Copy link
Copy Markdown
Member

Thank you again for your patience with this process, @wheevu ! I hope you can forgive me for all the suggested test name changes; I was trying to make sure they're consistent, and that they indicate precisely which path they're hitting, but please let me know what you think (e.g. more concise, consistent, and descriptive alternatives) 🎉

Also, if you think there's any subtleties that are hard to succinctly capture in the test names, may you please add short docstring that describes the various parameterizations or any assumptions that might be baked into the test?

Hi Josh (@wheevu), Wanda and I have talked through the comments, and Wanda is just going to make a few small changes documented in the comments, to expedite completion of this PR. I think we should leave out the docstring requirement for now because it's not on any of the other tests, and in these tests we have a good idea of what is being tested from the good method names being used. There were some very cool tests in here, Josh (also noted in the comments, some of which you'll have to dig into because the conversations are resolved). Thank you for taking on this enhancement (and the issues in your other PRs).

Comment thread planarity/express/planarity.pyx
@wheevu

wheevu commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Thank you again for your patience with this process, @wheevu ! I hope you can forgive me for all the suggested test name changes; I was trying to make sure they're consistent, and that they indicate precisely which path they're hitting, but please let me know what you think (e.g. more concise, consistent, and descriptive alternatives) 🎉
Also, if you think there's any subtleties that are hard to succinctly capture in the test names, may you please add short docstring that describes the various parameterizations or any assumptions that might be baked into the test?

Hi Josh (@wheevu), Wanda and I have talked through the comments, and Wanda is just going to make a few small changes documented in the comments, to expedite completion of this PR. I think we should leave out the docstring requirement for now because it's not on any of the other tests, and in these tests we have a good idea of what is being tested from the good method names being used. There were some very cool tests in here, Josh (also noted in the comments, some of which you'll have to dig into because the conversations are resolved). Thank you for taking on this enhancement (and the issues in your other PRs).

Thanks, John and Wanda! I'll take some time to read through all the comments properly. There's a lot for me to learn from the discussion. I really appreciate you both working through this and taking care of the final changes. Glad the tests were useful! 😄

@wbkboyer

Copy link
Copy Markdown
Member

Thank you again for your patience with this process, @wheevu ! I hope you can forgive me for all the suggested test name changes; I was trying to make sure they're consistent, and that they indicate precisely which path they're hitting, but please let me know what you think (e.g. more concise, consistent, and descriptive alternatives) 🎉
Also, if you think there's any subtleties that are hard to succinctly capture in the test names, may you please add short docstring that describes the various parameterizations or any assumptions that might be baked into the test?

Hi Josh (@wheevu), Wanda and I have talked through the comments, and Wanda is just going to make a few small changes documented in the comments, to expedite completion of this PR. I think we should leave out the docstring requirement for now because it's not on any of the other tests, and in these tests we have a good idea of what is being tested from the good method names being used. There were some very cool tests in here, Josh (also noted in the comments, some of which you'll have to dig into because the conversations are resolved). Thank you for taking on this enhancement (and the issues in your other PRs).

Thanks, John and Wanda! I'll take some time to read through all the comments properly. There's a lot for me to learn from the discussion. I really appreciate you both working through this and taking care of the final changes. Glad the tests were useful! 😄

No worries at all; it's my (and our) pleasure!! It's wonderful to have help coming on on these issues, and your null byte detection and handling + unicode filename support are excellent finds! I'll go through and make the remainder of these small changes, then will get this merged in, so no need to do anything on your end <3

I'll keep ya posted if we need anything on the subsequent PRs 🎉 Thank you again, Josh (@wheevu )

Changed order of parameters so that `labels` comes after `output_type`which comes after `outfileName` in `PGraph.draw()` parameterization, so I updated all callers to this method, docstring order, tests, etc. Also updated all tests and examples calling `draw()` that include image configuration options so that they appear in the same order as the docstring + code.

Added info for how to suppress DeprecationWarning in ascii() warning messages.

Rearranged `Raises` blocks to try to capture the order in which one might encounter those exceptions in the code.

Added extra assert to `test_label_type_index_matches_mapping_and_text_drawing()` since we know that when `outfileName` is `None`, we expect to receive a string.
@wbkboyer
wbkboyer self-requested a review October 20, 2026 23:56

@wbkboyer wbkboyer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to rock! Thank you again, @wheevu 🎉

@wbkboyer
wbkboyer merged commit d6b2c66 into graph-algorithms:master Oct 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Express API: Small refactor of APIs for textual drawing of planar graphs

3 participants