Repository: jorenham/optype (language: python)

Issue:
`optype.string`  with type-aliases for the constants in `string`



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index b733a80a..4f358b5d 100644
--- a/README.md
+++ b/README.md
@@ -233,6 +233,7 @@ The reference docs are structured as follows:
 - [`optype.copy`](#optypecopy)
 - [`optype.dataclasses`](#optypedataclasses)
 - [`optype.pickle`](#optypepickle)
+- [`optype.string`](#optypestring)
 - [`optype.typing`](#optypetyping)
     - [`Any*` type aliases](#any-type-aliases)
     - [`Empty*` type aliases](#empty-type-aliases)
@@ -1652,9 +1653,131 @@ interfaces:
     </tr>
 </table>
 
+### `optype.string`
+
+The [`string`](https://docs.python.org/3/library/string.html) standard
+library contains practical constants, but it has two issues:
+
+- The constants contain a collection of characters, but are represented as
+  a single string. This makes it practically impossible to type-hint the
+  individual characters, so typeshed currently types these constants as a
+  `LiteralString`.
+- The names of the constants are inconsistent, and doesn't follow
+  [PEP 8](https://peps.python.org/pep-0008/#constants).
+
+So instead, `optype.string` provides an alternative interface, that is
+compatible with `string`, but with slight differences:
+
+- For each constant, there is a corresponding `Literal` type alias for
+  the *individual* characters. Its name matches the name of the constant,
+  but is singular instead of plural.
+- Instead of a single string, `optype.string` uses a `tuple` of characters,
+  so that each character has its own `typing.Literal` annotation.
+  Note that this is only tested with (based)pyright / pylance, so it might
+  not work with mypy (it has more bugs than it has lines of codes).
+- The names of the constant are consistent with PEP 8, and use a postfix
+  notation for variants, e.g. `DIGITS_HEX` instead of `hexdigits`.
+- Unlike `string`, `optype.string` has a constant (and type alias) for
+  binary digits `'0'` and `'1'`; `DIGITS_BIN` (and `DigitBin`). Because
+  besides `oct` and `hex` functions in `builtins`, there's also the
+  `builtins.bin` function.
+
+<table>
+    <tr>
+        <th colspan="2"><code>string._</code></th>
+        <th colspan="2"><code>optype.string._</code></th>
+    </tr>
+    <tr>
+        <th>constant</th>
+        <th>char type</th>
+        <th>constant</th>
+        <th>char type</th>
+    </tr>
+    <tr>
+        <td colspan="2" align="center"><i>missing</i></td>
+        <td><code>DIGITS_BIN</code></td>
+        <td><code>DigitBin</code></td>
+    </tr>
+    <tr>
+        <td><code>octdigits</code></td>
+        <td rowspan="9"><code>LiteralString</code></td>
+        <td><code>DIGITS_OCT</code></td>
+        <td><code>DigitOct</code></td>
+    </tr>
+    <tr>
+        <td><code>digits</code></td>
+        <td><code>DIGITS</code></td>
+        <td><code>Digit</code></td>
+    </tr>
+    <tr>
+        <td><code>hexdigits</code></td>
+        <td><code>DIGITS_HEX</code></td>
+        <td><code>DigitHex</code></td>
+    </tr>
+    <tr>
+        <td><code>ascii_letters</code></td>
+        <td><code>LETTERS</code></td>
+        <td><code>Letter</code></td>
+    </tr>
+    <tr>
+        <td><code>ascii_lowercase</code></td>
+        <td><code>LETTERS_LOWER</code></td>
+        <td><code>LetterLower</code></td>
+    </tr>
+    <tr>
+        <td><code>ascii_uppercase</code></td>
+        <td><code>LETTERS_UPPER</code></td>
+        <td><code>LetterUpper</code></td>
+    </tr>
+    <tr>
+        <td><code>punctuation</code></td>
+        <td><code>PUNCTUATION</code></td>
+        <td><code>Punctuation</code></td>
+    </tr>
+    <tr>
+        <td><code>whitespace</code></td>
+        <td><code>WHITESPACE</code></td>
+        <td><code>Whitespace</code></td>
+    </tr>
+    <tr>
+        <td><code>printable</code></td>
+        <td><code>PRINTABLE</code></td>
+        <td><code>Printable</code></td>
+    </tr>
+</table>
+
+Each of the `optype.string` constants is exactly the same as the corresponding
+`string` constant (after concatenation / splitting), e.g.
+
+```pycon
+>>> import string
+>>> import optype as opt
+>>> ''.join(opt.string.PRINTABLE) == string.printable
+True
+>>> tuple(string.printable) == opt.string.PRINTABLE
+True
+```
+
+Similarly, the values within a constant's `Literal` type exactly match the
+values of its constant:
+
+```pycon
+>>> import optype as opt
+>>> from optype.inspect import get_args
+>>> get_args(opt.string.Printable) == opt.string.PRINTABLE
+True
+```
+
+The `optype.inspect.get_args` is a non-broken variant of `typing.get_args`
+that correctly flattens nested literals, type-unions, and PEP 695 type aliases,
+so that it matches the official typing specs.
+*In other words; `typing.get_args` is yet another fundamentally broken
+python-typing feature that's useless in the situations where you need it
+most.*
+
 ### `optype.typing`
 
-### `Any*` type aliases
+#### `Any*` type aliases
 
 Type aliases for anything that can *always* be passed to
 `int`, `float`, `complex`, `iter`, or `typing.Literal`
@@ -1691,7 +1814,7 @@ Type aliases for anything that can *always* be passed to
 > `complex`, most of them can't, and are therefore not included in these
 > type aliases.
 
-### `Empty*` type aliases
+#### `Empty*` type aliases
 
 These are builtin types or collections that are empty, i.e. have length 0 or
 yield no elements.
@@ -1731,7 +1854,7 @@ yield no elements.
     </tr>
 </table>
 
-### Literal types
+#### Literal types
 
 <table>
     <tr>
diff --git a/optype/__init__.py b/optype/__init__.py
index af77ee92..d5ab8e80 100644
--- a/optype/__init__.py
+++ b/optype/__init__.py
@@ -309,12 +309,13 @@
     'inspect',
     'pickle',
     'rick',
+    'string',
     'typing',
 )
 
 from importlib import metadata as _metadata
 
-from . import copy, dataclasses, inspect, pickle, typing
+from . import copy, dataclasses, inspect, pickle, string, typing
 from ._can import (
     CanAEnter,
     CanAEnterSelf,
diff --git a/optype/inspect.py b/optype/inspect.py
index 0c8f93db..6d1b4ab0 100644
--- a/optype/inspect.py
+++ b/optype/inspect.py
@@ -93,7 +93,9 @@ def get_args(tp: TypeAliasType | type | str | Any, /) -> tuple[Any, ...]:
     - recursively flattens nested of union'ed type aliases.
     """
     args = _get_args(tp.__value__ if isinstance(tp, TypeAliasType) else tp)
-    return tuple(itertools.chain(*(get_args(arg) or [arg] for arg in args)))
+    return tuple(
+        itertools.chain.from_iterable(get_args(arg) or [arg] for arg in args),
+    )
 
 
 def get_protocol_members(cls: type, /) -> frozenset[str]:
diff --git a/optype/string.py b/optype/string.py
new file mode 100644
index 00000000..a3731221
--- /dev/null
+++ b/optype/string.py
@@ -0,0 +1,105 @@
+"""
+Type aliases for the `strings` standard library.
+
+See Also:
+    - https://docs.python.org/3/library/string.html
+"""
+from __future__ import annotations
+
+from typing import Final, Literal as L, TypeAlias  # noqa: N817
+
+
+__all__ = (
+    'DIGITS',
+    'DIGITS_BIN',
+    'DIGITS_HEX',
+    'DIGITS_OCT',
+    'LETTERS',
+    'LETTERS_LOWER',
+    'LETTERS_UPPER',
+    'PRINTABLE',
+    'PUNCTUATION',
+    'WHITESPACE',
+
+    'Digit',
+    'DigitBin',
+    'DigitHex',
+    'DigitOct',
+    'Letter',
+    'LetterLower',
+    'LetterUpper',
+    'Printable',
+    'Punctuation',
+    'Whitespace',
+)
+
+
+DigitBin: TypeAlias = L['0', '1']
+DIGITS_BIN: Final = '0', '1'
+
+# compatible with `string.octdigits`
+DigitOct: TypeAlias = L['0', '1', '2', '3', '4', '5', '6', '7']
+DIGITS_OCT: Final = '0', '1', '2', '3', '4', '5', '6', '7'
+
+# compatible with `string.hexdigits`
+DigitHex: TypeAlias = L[
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+    'a', 'b', 'c', 'd', 'e', 'f',
+    'A', 'B', 'C', 'D', 'E', 'F',
+]  # fmt: skip
+DIGITS_HEX: Final = (
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+    'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F',
+)  # fmt: skip
+
+# compatible with `string.digits`
+Digit: TypeAlias = L['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
+DIGITS: Final = '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
+
+
+# compatible with `string.ascii_letters`
+LetterLower: TypeAlias = L[
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+    'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+]  # fmt: skip
+LETTERS_LOWER: Final = (
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+    'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+)  # fmt: skip
+
+# compatible with `string.ascii_lowercase`
+LetterUpper: TypeAlias = L[
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+]  # fmt: skip
+LETTERS_UPPER: Final = (
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+)  # fmt: skip
+
+# compatible with `string.ascii_letters`
+Letter: TypeAlias = LetterLower | LetterUpper
+LETTERS: Final = LETTERS_LOWER + LETTERS_UPPER
+
+
+# compatible with `string.punctuation`
+Punctuation: TypeAlias = L[
+    '!', '"', '#', '$', '%', '&', "'", '(',
+    ')', '*', '+', ',', '-', '.', '/', ':',
+    ';', '<', '=', '>', '?', '@', '[', '\\',
+    ']', '^', '_', '`', '{', '|', '}', '~',
+]  # fmt: skip
+PUNCTUATION: Final = (
+    '!', '"', '#', '$', '%', '&', "'", '(',
+    ')', '*', '+', ',', '-', '.', '/', ':',
+    ';', '<', '=', '>', '?', '@', '[', '\\',
+    ']', '^', '_', '`', '{', '|', '}', '~',
+)  # fmt: skip
+
+# compatible with `string.whitespace`
+Whitespace: TypeAlias = L[' ', '\t', '\n', '\r', '\v', '\f']
+WHITESPACE: Final = ' ', '\t', '\n', '\r', '\v', '\f'
+
+# compatible with `string.printable`
+Printable: TypeAlias = Digit | Letter | Punctuation | Whitespace
+PRINTABLE: Final = DIGITS + LETTERS + PUNCTUATION + WHITESPACE


Test patch:
diff --git a/tests/test_string.py b/tests/test_string.py
new file mode 100644
index 00000000..2d7b63d3
--- /dev/null
+++ b/tests/test_string.py
@@ -0,0 +1,58 @@
+import string
+from typing import Final
+
+import pytest
+from typing_extensions import LiteralString, Protocol, TypeVar
+
+import optype as opt
+
+
+@pytest.mark.parametrize(
+    ('opt_str', 'std_str'),
+    [
+        (opt.string.DIGITS_OCT, string.octdigits),
+        (opt.string.DIGITS, string.digits),
+        (opt.string.DIGITS_HEX, string.hexdigits),
+        (opt.string.LETTERS_LOWER, string.ascii_lowercase),
+        (opt.string.LETTERS_UPPER, string.ascii_uppercase),
+        (opt.string.LETTERS, string.ascii_letters),
+        (opt.string.PUNCTUATION, string.punctuation),
+        (opt.string.WHITESPACE, string.whitespace),
+        (opt.string.PRINTABLE, string.printable),
+    ],
+)
+def test_optype_eq_stdlib(
+    opt_str: tuple[LiteralString, ...],
+    std_str: LiteralString,
+):
+    assert opt_str == tuple(std_str)
+
+
+_ArgT = TypeVar('_ArgT', bound=opt.typing.AnyLiteral, infer_variance=True)
+
+
+# a `typing.Literal` stores it's values in `__args__`
+class _HasArgs(Protocol[_ArgT]):
+    __args__: Final[tuple[_ArgT, ...]]
+
+
+@pytest.mark.parametrize(
+    ('const', 'lit'),
+    [
+        (opt.string.DIGITS_BIN, opt.string.DigitBin),
+        (opt.string.DIGITS_OCT, opt.string.DigitOct),
+        (opt.string.DIGITS, opt.string.Digit),
+        (opt.string.DIGITS_HEX, opt.string.DigitHex),
+        (opt.string.LETTERS_LOWER, opt.string.LetterLower),
+        (opt.string.LETTERS_UPPER, opt.string.LetterUpper),
+        (opt.string.LETTERS, opt.string.Letter),
+        (opt.string.PUNCTUATION, opt.string.Punctuation),
+        (opt.string.WHITESPACE, opt.string.Whitespace),
+        (opt.string.PRINTABLE, opt.string.Printable),
+    ],
+)
+def test_literal_args_eq_constant(
+    const: tuple[LiteralString, ...],
+    lit: _HasArgs[LiteralString],
+):
+    assert opt.inspect.get_args(lit) == const