Skip to content

perf: SpecifierSet use tuple instead of frozenset for _specs#1108

Merged
notatallshaw merged 1 commit intopypa:mainfrom
notatallshaw:tuple-specifier-set
Mar 7, 2026
Merged

perf: SpecifierSet use tuple instead of frozenset for _specs#1108
notatallshaw merged 1 commit intopypa:mainfrom
notatallshaw:tuple-specifier-set

Conversation

@notatallshaw
Copy link
Copy Markdown
Member

@notatallshaw notatallshaw commented Mar 7, 2026

Change SpecifierSet._specs from frozenset to tuple as it is only ever iterated, never looked up, so tuple is a better fit, more performant, and doesn't cause hash seed based ordering. Strings are deduplicated before constructing Specifier objects to reduce the number of Specifiers constructed and hashing strings is much faster than hashing Specifiers.

Since tuples are order-sensitive unlike frozensets, __hash__, __eq__, and __str__ now lazily sort and cache _specs to ensure a canonical order.

@notatallshaw
Copy link
Copy Markdown
Member Author

notatallshaw commented Mar 7, 2026

Big improvement in construction time, mild improvement in resolver loop:

All benchmarks:
| Change   | Before [20f1523c]    | After [aef890ff]    |   Ratio | Benchmark (Parameter)                                                                               |
|----------|----------------------|---------------------|---------|-----------------------------------------------------------------------------------------------------|
|          | 5.06±0.03ms          | 5.08±0.04ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]          |
|          | 3.36±0.01ms          | 3.36±0.02ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]          |
|          | 4.00±0.03ms          | 3.96±0.03ms         |    0.99 | markers.TimeMarkerSuite.time_constructor [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]          |
|          | 3.79±0.04ms          | 3.80±0.04ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]          |
|          | 4.05±0.02ms          | 4.08±0.03ms         |    1.01 | markers.TimeMarkerSuite.time_constructor [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]          |
|          | 5.20±0.04ms          | 5.18±0.03ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]           |
|          | 5.44±0.05ms          | 5.45±0.04ms         |    1    | markers.TimeMarkerSuite.time_constructor [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]           |
|          | 1.64±0.01ms          | 1.63±0.01ms         |    1    | markers.TimeMarkerSuite.time_evaluate [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]             |
|          | 1.05±0.01ms          | 1.06±0.01ms         |    1.01 | markers.TimeMarkerSuite.time_evaluate [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]             |
|          | 1.23±0.01ms          | 1.23±0.01ms         |    1.01 | markers.TimeMarkerSuite.time_evaluate [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]             |
|          | 1.14±0.01ms          | 1.13±0.01ms         |    0.99 | markers.TimeMarkerSuite.time_evaluate [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]             |
|          | 1.16±0.01ms          | 1.18±0.01ms         |    1.01 | markers.TimeMarkerSuite.time_evaluate [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]             |
|          | 1.66±0.01ms          | 1.67±0.01ms         |    1    | markers.TimeMarkerSuite.time_evaluate [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]              |
|          | 1.62±0ms             | 1.62±0.01ms         |    1    | markers.TimeMarkerSuite.time_evaluate [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]              |
| -        | 22.8±0.1ms           | 18.1±0.08ms         |    0.8  | requirement.TimeRequirementSuite.time_constructor [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0] |
| -        | 16.2±0.06ms          | 12.6±0.08ms         |    0.78 | requirement.TimeRequirementSuite.time_constructor [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0] |
| -        | 18.4±0.1ms           | 14.5±0.1ms          |    0.79 | requirement.TimeRequirementSuite.time_constructor [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0] |
| -        | 17.3±0.07ms          | 13.7±0.09ms         |    0.79 | requirement.TimeRequirementSuite.time_constructor [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0] |
| -        | 17.8±0.2ms           | 14.0±0.1ms          |    0.79 | requirement.TimeRequirementSuite.time_constructor [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0] |
| -        | 23.9±0.2ms           | 18.6±0.05ms         |    0.78 | requirement.TimeRequirementSuite.time_constructor [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]  |
| -        | 24.2±0.2ms           | 19.1±0.2ms          |    0.79 | requirement.TimeRequirementSuite.time_constructor [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]  |
| -        | 1.13±0.01ms          | 1.02±0ms            |    0.9  | resolver.TimeResolverSuite.time_resolver_loop [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]     |
|          | 785±3μs              | 720±5μs             |    0.92 | resolver.TimeResolverSuite.time_resolver_loop [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]     |
|          | 806±7μs              | 733±3μs             |    0.91 | resolver.TimeResolverSuite.time_resolver_loop [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]     |
|          | 759±5μs              | 697±10μs            |    0.92 | resolver.TimeResolverSuite.time_resolver_loop [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]     |
|          | 753±3μs              | 690±5μs             |    0.92 | resolver.TimeResolverSuite.time_resolver_loop [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]     |
|          | 1.21±0.01ms          | 1.11±0ms            |    0.92 | resolver.TimeResolverSuite.time_resolver_loop [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]      |
|          | 1.19±0ms             | 1.10±0ms            |    0.93 | resolver.TimeResolverSuite.time_resolver_loop [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]      |
| -        | 5.83±0.04ms          | 2.11±0.01ms         |    0.36 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]         |
| -        | 4.30±0.03ms          | 1.86±0.01ms         |    0.43 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]         |
| -        | 4.68±0.03ms          | 2.12±0.02ms         |    0.45 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]         |
| -        | 4.34±0.05ms          | 2.03±0.01ms         |    0.47 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]         |
| -        | 4.27±0.05ms          | 2.04±0.01ms         |    0.48 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]         |
| -        | 6.21±0.05ms          | 2.10±0.01ms         |    0.34 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]          |
| -        | 6.07±0.03ms          | 2.05±0.01ms         |    0.34 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]          |
|          | 7.65±0.03ms          | 7.76±0.03ms         |    1.02 | specifiers.TimeSpecSuite.time_contains_cold [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]       |
|          | 5.58±0.03ms          | 5.40±0.06ms         |    0.97 | specifiers.TimeSpecSuite.time_contains_cold [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]       |
|          | 6.06±0.03ms          | 5.90±0.03ms         |    0.97 | specifiers.TimeSpecSuite.time_contains_cold [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]       |
|          | 5.76±0.03ms          | 5.47±0.03ms         |    0.95 | specifiers.TimeSpecSuite.time_contains_cold [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]       |
|          | 5.57±0.03ms          | 5.38±0.06ms         |    0.97 | specifiers.TimeSpecSuite.time_contains_cold [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]       |
|          | 8.32±0.08ms          | 8.40±0.02ms         |    1.01 | specifiers.TimeSpecSuite.time_contains_cold [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]        |
|          | 8.25±0.04ms          | 8.35±0.04ms         |    1.01 | specifiers.TimeSpecSuite.time_contains_cold [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]        |
|          | 5.68±0.03ms          | 5.82±0.03ms         |    1.02 | specifiers.TimeSpecSuite.time_contains_warm [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]       |
|          | 4.24±0.02ms          | 4.17±0.02ms         |    0.98 | specifiers.TimeSpecSuite.time_contains_warm [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]       |
|          | 4.55±0.02ms          | 4.42±0.01ms         |    0.97 | specifiers.TimeSpecSuite.time_contains_warm [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]       |
|          | 4.26±0.04ms          | 4.10±0.03ms         |    0.96 | specifiers.TimeSpecSuite.time_contains_warm [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]       |
|          | 4.20±0.03ms          | 4.19±0.03ms         |    1    | specifiers.TimeSpecSuite.time_contains_warm [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]       |
|          | 6.09±0.05ms          | 6.26±0.02ms         |    1.03 | specifiers.TimeSpecSuite.time_contains_warm [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]        |
|          | 5.99±0.03ms          | 6.20±0.02ms         |    1.03 | specifiers.TimeSpecSuite.time_contains_warm [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]        |
|          | 16.9±0.2μs           | 16.8±0.2μs          |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]  |
|          | 11.0±0.08μs          | 11.0±0.02μs         |    1    | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]  |
|          | 9.43±0.04μs          | 9.42±0.04μs         |    1    | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]  |
|          | 9.10±0.1μs           | 9.04±0.05μs         |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]  |
|          | 8.93±0.1μs           | 9.01±0.05μs         |    1.01 | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]  |
|          | 18.1±0.2μs           | 18.0±0.07μs         |    1    | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]   |
|          | 17.9±0.1μs           | 17.9±0.06μs         |    1    | specifiers.TimeSpecSuite.time_filter_simple_cold [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]   |
|          | 14.8±0.03μs          | 14.4±0.05μs         |    0.97 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]  |
|          | 9.44±0.03μs          | 9.49±0.05μs         |    1.01 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]  |
|          | 7.74±0.09μs          | 7.65±0.03μs         |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]  |
|          | 7.55±0.05μs          | 7.58±0.06μs         |    1    | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]  |
|          | 7.57±0.06μs          | 7.60±0.04μs         |    1    | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]  |
|          | 15.8±0.1μs           | 15.5±0.09μs         |    0.98 | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]   |
|          | 15.3±0.1μs           | 15.4±0.08μs         |    1    | specifiers.TimeSpecSuite.time_filter_simple_warm [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]   |
|          | 5.02±0.01μs          | 4.94±0.02μs         |    0.98 | utils.TimeUtils.time_canonicalize_name [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]            |
|          | 3.39±0.02μs          | 3.39±0.01μs         |    1    | utils.TimeUtils.time_canonicalize_name [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]            |
|          | 3.52±0.01μs          | 3.54±0.02μs         |    1.01 | utils.TimeUtils.time_canonicalize_name [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]            |
|          | 3.66±0.02μs          | 3.69±0.04μs         |    1.01 | utils.TimeUtils.time_canonicalize_name [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]            |
|          | 3.93±0.02μs          | 3.94±0.01μs         |    1    | utils.TimeUtils.time_canonicalize_name [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]            |
|          | 5.54±0.02μs          | 5.60±0.02μs         |    1.01 | utils.TimeUtils.time_canonicalize_name [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]             |
|          | 5.34±0.02μs          | 5.33±0.01μs         |    1    | utils.TimeUtils.time_canonicalize_name [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]             |
|          | 2.51±0.02ms          | 2.50±0.02ms         |    1    | version.TimeVersionSuite.time_constructor [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]         |
|          | 1.65±0.01ms          | 1.62±0.01ms         |    0.98 | version.TimeVersionSuite.time_constructor [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]         |
|          | 2.04±0.02ms          | 2.05±0.01ms         |    1    | version.TimeVersionSuite.time_constructor [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]         |
|          | 1.82±0.03ms          | 1.84±0.02ms         |    1.01 | version.TimeVersionSuite.time_constructor [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]         |
|          | 1.66±0.01ms          | 1.66±0.01ms         |    1    | version.TimeVersionSuite.time_constructor [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]         |
|          | 2.65±0.01ms          | 2.65±0.01ms         |    1    | version.TimeVersionSuite.time_constructor [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]          |
|          | 2.61±0.02ms          | 2.63±0.02ms         |    1.01 | version.TimeVersionSuite.time_constructor [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]          |
|          | 4.30±0.01ms          | 4.29±0.02ms         |    1    | version.TimeVersionSuite.time_hash [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]                |
|          | 2.85±0.02ms          | 2.84±0.02ms         |    1    | version.TimeVersionSuite.time_hash [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]                |
|          | 3.40±0.01ms          | 3.41±0.01ms         |    1    | version.TimeVersionSuite.time_hash [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]                |
|          | 3.18±0.02ms          | 3.21±0.04ms         |    1.01 | version.TimeVersionSuite.time_hash [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]                |
|          | 3.17±0.02ms          | 3.17±0.02ms         |    1    | version.TimeVersionSuite.time_hash [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]                |
|          | 4.51±0.04ms          | 4.51±0.03ms         |    1    | version.TimeVersionSuite.time_hash [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]                 |
|          | 4.41±0.03ms          | 4.41±0.03ms         |    1    | version.TimeVersionSuite.time_hash [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]                 |
|          | 2.60±0.02ms          | 2.62±0.01ms         |    1.01 | version.TimeVersionSuite.time_sort_cold [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]           |
|          | 1.81±0.01ms          | 1.80±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_cold [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]           |
|          | 2.06±0.01ms          | 2.06±0.01ms         |    1    | version.TimeVersionSuite.time_sort_cold [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]           |
|          | 2.13±0.01ms          | 2.14±0.01ms         |    1    | version.TimeVersionSuite.time_sort_cold [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]           |
|          | 2.20±0ms             | 2.21±0.01ms         |    1    | version.TimeVersionSuite.time_sort_cold [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]           |
|          | 3.38±0.02ms          | 3.37±0.02ms         |    1    | version.TimeVersionSuite.time_sort_cold [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]            |
|          | 3.34±0.04ms          | 3.27±0.02ms         |    0.98 | version.TimeVersionSuite.time_sort_cold [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]            |
|          | 2.07±0.02ms          | 2.05±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]           |
|          | 1.48±0.01ms          | 1.49±0.01ms         |    1.01 | version.TimeVersionSuite.time_sort_warm [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]           |
|          | 1.71±0.01ms          | 1.70±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]           |
|          | 1.75±0.01ms          | 1.77±0.01ms         |    1.01 | version.TimeVersionSuite.time_sort_warm [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]           |
|          | 1.85±0.01ms          | 1.85±0.01ms         |    1    | version.TimeVersionSuite.time_sort_warm [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]           |
|          | 2.70±0.02ms          | 2.71±0.02ms         |    1    | version.TimeVersionSuite.time_sort_warm [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]            |
|          | 2.67±0.03ms          | 2.64±0.01ms         |    0.99 | version.TimeVersionSuite.time_sort_warm [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]            |
|          | 4.23±0.03ms          | 4.17±0.02ms         |    0.98 | version.TimeVersionSuite.time_str [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]                 |
|          | 2.98±0.02ms          | 2.99±0.01ms         |    1    | version.TimeVersionSuite.time_str [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]                 |
|          | 3.32±0.01ms          | 3.33±0.01ms         |    1.01 | version.TimeVersionSuite.time_str [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]                 |
|          | 2.92±0.07ms          | 2.91±0.03ms         |    0.99 | version.TimeVersionSuite.time_str [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]                 |
|          | 2.88±0.03ms          | 2.89±0.03ms         |    1    | version.TimeVersionSuite.time_str [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]                 |
|          | 4.66±0.01ms          | 4.64±0.03ms         |    1    | version.TimeVersionSuite.time_str [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]                  |
|          | 4.61±0.02ms          | 4.62±0.02ms         |    1    | version.TimeVersionSuite.time_str [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]                  |
SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE INCREASED.

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented Mar 7, 2026

Nice, creating sets is expensive.

@notatallshaw notatallshaw merged commit f59d6ee into pypa:main Mar 7, 2026
56 checks passed
@notatallshaw
Copy link
Copy Markdown
Member Author

The moment I merged this I thought of a possible regression in the way I'm deduping specifiers :(. I might have a follow up PR shortly.

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