Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[widget audit] toga.Button #1761

Merged
merged 66 commits into from
Mar 21, 2023
Merged

[widget audit] toga.Button #1761

merged 66 commits into from
Mar 21, 2023

Conversation

freakboy3742
Copy link
Member

@freakboy3742 freakboy3742 commented Jan 31, 2023

A testing and documentation audit of the toga.Button widget.

Includes the content from #1794.

Audit checklist

  • Core tests ported to pytest
  • Core tests at 100% coverage
  • Core docstrings complete, and in Sphinx format
  • Documentation complete and consistently formatted
  • cocoa backend at 100% coverage
  • winforms backend at 100% coverage
  • gtk backend at 100% coverage
  • iOS backend at 100% coverage
  • android backend at 100% coverage
  • Widget support table updated

@freakboy3742
Copy link
Member Author

As of cddb9e4 platform coverage is at:

  • core 100%
  • cocoa 100%
  • winforms 33.3% (?)
  • iOS 86.7%
  • Android 98.0%
  • Linux 85.7%

Most of the coverage omissions appear to be related to the button press. I suspect coverage on Windows is a misreport; it's not reporting coverage on any of the method bodies, even though the tests should be running. This might be a path issue in my virtualised Windows setup.

@freakboy3742 freakboy3742 marked this pull request as draft January 31, 2023 06:25
@mhsmith
Copy link
Member

mhsmith commented Jan 31, 2023

The Windows issue was caused by the same thing as nedbat/coveragepy#686: the main loop runs on a thread that's created through the Windows API, not the Python threading API, so it wasn't detected by coverage.

Fixed by manually installing the trace hook at the start of the thread.

(50, 128, 200, 0.0),
(50, 128, 200, 0.5),
(50, 128, 200, 0.9),
(50, 128, 200, 1.0),
Copy link
Member Author

Choose a reason for hiding this comment

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

I've reduced the multiplicity of colours here as it didn't seem to be adding anything to test rigor, but it was difficult to see the "meaningful" colors appear in the test series.

@freakboy3742
Copy link
Member Author

As of 065bca3, the GTK colour tests pass, but only if you have UI animations turned of. At the command line, run:

gsettings set org.gnome.desktop.interface enable-animations false

You can reset the setting with:

gsettings reset org.gnome.desktop.interface enable-animations false

This is an OS-wide setting.

@freakboy3742 freakboy3742 force-pushed the audit-button branch 4 times, most recently from 227e187 to 4a1ce8a Compare February 16, 2023 05:24
@mhsmith
Copy link
Member

mhsmith commented Feb 19, 2023

On Debian Buster, after disabling animations, I was able to reproduce some similar failures to the CI, plus a new one involving fonts and UTF-8:

Log
[testbed] Running test suite in dev environment...
===========================================================================
WARNING: Can't find icon resources/testbed; falling back to default icon
/home/smith/git/beeware/toga/gtk/src/toga_gtk/app.py:43: DeprecationWarning: Gtk.Window.set_wmclass is deprecated
  self.native.set_wmclass(app.interface.name, app.interface.name)

(testbed.py:15361): Pango-CRITICAL **: 20:21:45.025: pango_font_description_set_size: assertion 'size >= 0' failed
============================= test session starts ==============================
collecting ... collected 25 items

tests/widgets/test_button.py::test_background_color <- tests/widgets/properties.py FAILED [  4%]
tests/widgets/test_button.py::test_background_color_reset <- tests/widgets/properties.py FAILED [  8%]
tests/widgets/test_button.py::test_color <- tests/widgets/properties.py FAILED [ 12%]
tests/widgets/test_button.py::test_color_reset <- tests/widgets/properties.py FAILED [ 16%]
tests/widgets/test_button.py::test_font <- tests/widgets/properties.py FAILED [ 20%]
tests/widgets/test_button.py::test_text <- tests/widgets/properties.py PASSED [ 24%]
tests/widgets/test_button.py::test_text_width_change <- tests/widgets/properties.py PASSED [ 28%]
tests/widgets/test_button.py::test_press PASSED                          [ 32%]
tests/widgets/test_button.py::test_background_color_transparent PASSED   [ 36%]
tests/widgets/test_button.py::test_button_size PASSED                    [ 40%]
tests/widgets/test_label.py::test_background_color <- tests/widgets/properties.py FAILED [ 44%]
tests/widgets/test_label.py::test_background_color_reset <- tests/widgets/properties.py FAILED [ 48%]
tests/widgets/test_label.py::test_background_color_transparent <- tests/widgets/properties.py PASSED [ 52%]
tests/widgets/test_label.py::test_color <- tests/widgets/properties.py FAILED [ 56%]
tests/widgets/test_label.py::test_color_reset <- tests/widgets/properties.py FAILED [ 60%]
tests/widgets/test_label.py::test_font <- tests/widgets/properties.py FAILED [ 64%]
tests/widgets/test_label.py::test_text <- tests/widgets/properties.py PASSED [ 68%]
tests/widgets/test_label.py::test_text_width_change <- tests/widgets/properties.py SKIPPED (resizes not applying correctly) [ 72%]
tests/widgets/test_label.py::test_multiline SKIPPED (changing text does not trigger a refresh (#1289)) [ 76%]
tests/widgets/test_label.py::test_alignment SKIPPED (alignment probe not implemented) [ 80%]
tests/widgets/test_slider.py::test_init PASSED                           [ 84%]
tests/widgets/test_slider.py::test_value SKIPPED (on_change called 2 times) [ 88%]
tests/widgets/test_slider.py::test_change SKIPPED (min/max value not correct) [ 92%]
tests/widgets/test_slider.py::test_min PASSED                            [ 96%]
tests/widgets/test_slider.py::test_max PASSED                            [100%]

=================================== FAILURES ===================================
____________________________ test_background_color _____________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 102, in test_background_color
    assert_color(probe.background_color, color)
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AttributeError: 'NoneType' object has no attribute 'r'
_________________________ test_background_color_reset __________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 113, in test_background_color_reset
    assert_color(probe.background_color, named_color(RED))
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AttributeError: 'NoneType' object has no attribute 'r'
__________________________________ test_color __________________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 74, in test_color
    assert_color(probe.color, color)
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AssertionError: assert 54 == 0
 +  where 54 = getattr(rgba(0, 0, 54, 1.0), 'b')
 +  and   0 = getattr(rgba(0, 0, 0, 1.0), 'b')
_______________________________ test_color_reset _______________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 89, in test_color_reset
    assert_color(probe.color, named_color(RED))
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AssertionError: assert 0 == 255
 +  where 0 = getattr(rgba(0, 0, 54, 1.0), 'r')
 +  and   255 = getattr(rgb(255, 0, 0), 'r')
__________________________________ test_font ___________________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 47, in test_font
    new_size_font = probe.font
  File "/home/smith/git/beeware/toga/gtk/tests_backend/widgets/base.py", line 78, in font
    return toga_font(sc.get_property("font", sc.get_state()))
  File "/home/smith/git/beeware/toga/gtk/tests_backend/widgets/properties.py", line 28, in toga_font
    family=font.get_family(),
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0 in position 0: invalid start byte
____________________________ test_background_color _____________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 102, in test_background_color
    assert_color(probe.background_color, color)
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AssertionError: assert 0 == 128
 +  where 0 = getattr(rgba(0, 0, 128, 1.0), 'r')
 +  and   128 = getattr(rgba(128, 128, 128, 1.0), 'r')
_________________________ test_background_color_reset __________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 113, in test_background_color_reset
    assert_color(probe.background_color, named_color(RED))
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AssertionError: assert 0 == 255
 +  where 0 = getattr(rgba(0, 0, 0, 1.0), 'r')
 +  and   255 = getattr(rgb(255, 0, 0), 'r')
__________________________________ test_color __________________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 74, in test_color
    assert_color(probe.color, color)
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AssertionError: assert 0 == 128
 +  where 0 = getattr(rgba(0, 0, 128, 1.0), 'r')
 +  and   128 = getattr(rgba(128, 128, 128, 1.0), 'r')
_______________________________ test_color_reset _______________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 89, in test_color_reset
    assert_color(probe.color, named_color(RED))
  File "/home/smith/git/beeware/toga/testbed/tests/assertions.py", line 26, in assert_color
    assert getattr(actual, component) == getattr(expected, component)
AssertionError: assert 0 == 255
 +  where 0 = getattr(rgba(0, 0, 0, 1.0), 'r')
 +  and   255 = getattr(rgb(255, 0, 0), 'r')
__________________________________ test_font ___________________________________
Traceback (most recent call last):
  File "/home/smith/git/beeware/toga/testbed/tests/widgets/properties.py", line 40, in test_font
    orig_font = probe.font
  File "/home/smith/git/beeware/toga/gtk/tests_backend/widgets/base.py", line 78, in font
    return toga_font(sc.get_property("font", sc.get_state()))
  File "/home/smith/git/beeware/toga/gtk/tests_backend/widgets/properties.py", line 28, in toga_font
    family=font.get_family(),
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x90 in position 0: invalid start byte
==================================== PASSES ====================================
__________________________________ test_init ___________________________________
---------------------------- Captured stdout setup -----------------------------
[GTK+] Not implemented: Slider.tick_count()
[GTK+] Not implemented: Slider.set_on_press()
[GTK+] Not implemented: Slider.set_on_release()
___________________________________ test_min ___________________________________
---------------------------- Captured stdout setup -----------------------------
[GTK+] Not implemented: Slider.tick_count()
[GTK+] Not implemented: Slider.set_on_press()
[GTK+] Not implemented: Slider.set_on_release()
___________________________________ test_max ___________________________________
---------------------------- Captured stdout setup -----------------------------
[GTK+] Not implemented: Slider.tick_count()
[GTK+] Not implemented: Slider.set_on_press()
[GTK+] Not implemented: Slider.set_on_release()
=================== 10 failed, 10 passed, 5 skipped in 3.38s ===================

@freakboy3742
Copy link
Member Author

Most of those errors look like the Pygobject issue - The font bug, and the failures where there's a B and A channel value, but no R and G value (e.g., test_color). Are you sure you're running with the bugfix fork of pygobject?

However, some of the other failures do give me an idea - I wonder if the issue is color resets, and the tests not correctly identifying the background color (which is essentially platform dependent, but will be reliably reproducible on any given machine, and appears to be rgba(0,0,0,0 on my machine).

@freakboy3742
Copy link
Member Author

Nope. It was Wayland. Of course it was.

@freakboy3742
Copy link
Member Author

The good news is that the global gsettings change is no longer needed; I've found a programmatic GTK setting that both X and Wayland honor.

@mhsmith
Copy link
Member

mhsmith commented Feb 20, 2023

OK, after pulling the current version of this PR, and running briefcase dev -r to change the pygobject version, everything now passes on my machine.

@mhsmith
Copy link
Member

mhsmith commented Mar 14, 2023

test_implementation doesn't allow widgets to implement methods by inheriting them from a base class. This is often the most readable and maintainable way of doing it, but will inevitably be different from platform to platform.

Also, it's fairly arbitrary which methods it's checking for: e.g. TextInput has a set_font method, but Label doesn't.

Now that we have the testbed, I think test_implementation is more trouble than it's worth, and can just be removed.

@mhsmith
Copy link
Member

mhsmith commented Mar 14, 2023

I've added a font_size example app for manual testing. Not all the widgets implement font size yet (that can wait until future PRs), but those that do are mostly consistent across the 5 platforms, and proportionate to the default font size.

The exception is Windows, where the default font size is 8 points. However, I think it's actually bigger than 8/72 inches on most screens, because it was fixed at a time when Windows assumed 96 dpi but the typical monitor was less dense than that (see this article from 1999). And since Windows is so serious about backward compatibility, we're still stuck with it. [EDIT 2023-09: Disregard that, I've now measured 8 point text on Windows 10 at 100% scale, and it's 11 pixels tall from top of ascender to bottom of descender, which is exactly what you would expect from 8 * 96 / 72 == 10.66.]

So Toga widgets with fixed font sizes currently appear proportionately larger on Windows than on the other platforms. I don't think there's anything we can do about that now, but in the future we could deal with it by implementing the CSS font size keywords small, medium, large, etc. Created #1814 to track this idea.

Copy link
Member Author

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

I've done a full review pass; this mostly looks good, but I've flagged a couple of minor issues. Most of them are likely fixed with a docstring explaining reasoning; the only "big" item is the dependency inversion on font family resolution in the test probe (and even that is that big).

# will give relative sizes that are consistent with desktop platforms. It also
# means font sizes will all change proportionately if the user adjusts the
# text size in the system settings.
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, self.interface.size)
Copy link
Member Author

Choose a reason for hiding this comment

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

This is definitely a good approximation; I only wonder if it might be worth introducing a fudge factor so that "12pt == 14sp" (i.e., "size in toga points" * 1.1666 == size in dp).

Copy link
Member Author

Choose a reason for hiding this comment

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

(for the record - this is on the "strong opinion, weakly held" list; I won't give much pushback on a different fudge factor, or no fudge factor at all).

Copy link
Member

@mhsmith mhsmith Mar 20, 2023

Choose a reason for hiding this comment

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

Windows defaults to 8 points (see above), and according to the fonts.py files in the respective backends, Cocoa defaults to 12 points and iOS 17. So I don't see any strong reason to fix 12 as the default. Android is already within the range of the other platforms, and making 1 Toga pt = 1sp brings it closer to iOS, which is probably the one it's most useful to be consistent with.

In the long run I think the best solution to this issue is the CSS font size keywords, as mentioned above.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure - that makes sense.

android/src/toga_android/fonts.py Show resolved Hide resolved

def get_typeface(self):
cache_key = (self.interface, default_typeface)
Copy link
Member Author

Choose a reason for hiding this comment

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

Why is the default typeface part of the cache key?

Copy link
Member

Choose a reason for hiding this comment

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

Because it may affect the resulting font when font_family is SYSTEM. The default font on most widgets is sans-serif, but on buttons it's sans-serif-medium, which is intermediate between normal and bold.

)

if font is None:
Copy link
Member Author

Choose a reason for hiding this comment

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

Are you certain this error handling is no longer required? Core and system fonts will have all the weights/variants; but if a user-supplied font doesn't have a particular variant/weight, will this still succeed?

(And if it will - a comment explaining how/why would be worthwhile)

Copy link
Member

Choose a reason for hiding this comment

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

It's no longer possible for font to be None here, because:

  • When a custom font can't be found, I've added a line above to fall back on the system font. Previously in this case, the size, style and weight settings would all be ignored.
  • According to the convertFont documentation if it fails to find a font with the given traits, it returns the original font unchanged. I'll add a comment explaining this.

Also, note I've changed the indentation here so that the SYSTEM font uses the same bold/italic logic as everything else. Previously the italic setting would be ignored with a SYSTEM font.

Copy link
Member

Choose a reason for hiding this comment

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

if a user-supplied font doesn't have a particular variant/weight

It would be good to have a test for this, but that would require actually supplying such a font, and it looks like loading fonts from a file is currently broken on Cocoa. But this won't be a very common thing for an app to do, so I'm happy to defer it until we come to test the Font API directly.

".AppleSystemUIFont",
"sans-serif-medium", # Android buttons
],
}
Copy link
Member Author

Choose a reason for hiding this comment

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

What's the reasoning behind having this table of "generic" fonts in the testbed assertions, rather than being something that is normalised out by the test backend? Previously, toga_font on each backend would normalise ".AppleSystemUIFont" into SYSTEM, but do so in the Cocoa backend.

Having this table here seems like an odd inversion - we've got platform-specific details in the generic test definitions.

Copy link
Member

Choose a reason for hiding this comment

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

The problem I was trying to solve was the many-to-one mapping between Toga font names and actual fonts. For example, SANS_SERIF and SYSTEM will often give you the same visible result, as will explicitly specifying the font name. But whether these 3 options can be distinguished by the probe varies from platform to platform.

But you're right, it would be better for platform-specific differences to be handled by the test backend, so I'll do that.

".AppleSystemUIFont": SYSTEM,
"Papyrus": FANTASY,
}.get(str(font.familyName), str(font.familyName)),
family=str(font.familyName),
Copy link
Member Author

Choose a reason for hiding this comment

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

See Q below about this inversion, removing the ".AppleSystemUIFont" normalisation from the platform backend.

if font is None:
print(f"Unable to load font: {self.interface.size}pt {full_name}")
else:
_FONT_CACHE[self.interface] = font
Copy link
Member Author

Choose a reason for hiding this comment

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

As with Cocoa; we either need to report the missing font issue, or docstring the reason why it isn't an issue.

await probe.redraw()
assert_font_family(probe.font.family, family)
assert probe.font.weight == weight
assert probe.font.style == style
Copy link
Member Author

Choose a reason for hiding this comment

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

This makes me wonder whether what is needed here is a set of comprehensive "font management" tests operating on a single widget (say, Label), and a basic "can a font be set" test on each widget. If basic font setting works on a widget, we don't have any particular reason to believe that weight/variants will be problematic.

Copy link
Member

Choose a reason for hiding this comment

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

That's true if we assume that the widgets are all using a common font implementation, and using it correctly, but that's something that could easily be broken by accident in the future. So I think it's worth being comprehensive as long as it only adds a few seconds to the tests.

Comment on lines +66 to +67
assert 10 <= probe.width <= 150, f"Width ({probe.width}) not in range (10, 150)"
assert 10 <= probe.height <= 50, f"Height ({probe.height}) not in range (10, 50)"
Copy link
Member

Choose a reason for hiding this comment

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

As discussed, these messages should be generated automatically by pytest's "assertion rewriting", but that isn't currently working on Android. But since this PR is blocking all the other widget testing, let's fix that separately.

core/tests/widgets/test_activityindicator.py Outdated Show resolved Hide resolved
docs/reference/api/widgets/label.rst Outdated Show resolved Hide resolved
core/tests/widgets/test_progressbar.py Outdated Show resolved Hide resolved
…play a single space to prevent the widget from collapsing
Copy link
Member

@mhsmith mhsmith left a comment

Choose a reason for hiding this comment

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

From the user's point of view, banning empty text on buttons seems like an arbitrary restriction. I see the problem was that it caused the button height to collapse on Winforms, so I've worked around that by making it display a single space instead, which is visually identical, and only requires a small special case in the tests.

I saw a similar issue with empty labels on iOS last week, so I'll probably end up doing something similar when I review the Label PR tomorrow. However, in the case of a label with a background color, the difference between a space and an empty string may be visually significant, so maybe there's a Unicode zero-width character that would be better.

Comment on lines 32 to 39
Notes
-----
* Any "truthy" object (i.e, object that evaluates as ``True``) can be provided
as the text for the button; if the object is not a string, it will be
automatically converted to a string using ``str()``.

* The button text cannot contain newlines. Any content after the first newline
will be ignored.
Copy link
Member

Choose a reason for hiding this comment

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

I've moved the property documentation into the docstring so it'll appear next to the property itself, rather than in a separate section.

Comment on lines 18 to 21
async def test_text_empty(widget, probe):
"The text displayed on a widget can be empty"
# Set the text to the empty string
widget.text = ""
Copy link
Member

Choose a reason for hiding this comment

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

Empty strings are now covered by test_text above, and conversion of non-string objects is covered by test_button.py in the core.

Copy link
Member Author

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

A couple more notes and tweaks, hopefully not too controversial.

if expected == SYSTEM:
assert actual == (
"sans-serif-medium"
if isinstance(self.widget, toga.Button)
Copy link
Member Author

Choose a reason for hiding this comment

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

We can use subclassing here, rather than an isinstance check.


self._impl.set_text(value)
self._impl.set_text(self._text)
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm generally against the introduction of interface state/shadow variables; in this case, the shadow variable is only required if we have problems round-tripping the original value. In this implementation we're not round tripping anyway (because we're losing None and multiline input); to support the winforms edge case, I'm entirely comfortable with a normalizing a user-supplied "\u0200" (zero width space) to ""). It's highly unlikely to ever be user supplied in that form, and it's functionally indistinguishable from "".

Copy link
Member

Choose a reason for hiding this comment

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

I'm entirely comfortable with a normalizing a user-supplied "\u0200" (zero width space) to "").

Makes sense, but \u0200 is LATIN CAPITAL LETTER A WITH DOUBLE GRAVE. I've replaced that with \u200B.

@@ -29,14 +29,8 @@ A button has a text label. A handler can be associated with button press events.

button = toga.Button('Click me', on_press=my_callback)

Notes
Copy link
Member Author

Choose a reason for hiding this comment

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

I think we should keep this as a "Notes" section. While the notes at present are all style related, we're very likely going to come across platform specific eccentricities that don't fit the description being "style", and don't map to a specific attribute like the text handling of None et al.

@mhsmith mhsmith merged commit fa8ae75 into beeware:main Mar 21, 2023
@freakboy3742 freakboy3742 deleted the audit-button branch March 21, 2023 22:35
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.

2 participants