"""
Utilities
---------
.. autoclass:: SupportsLt
.. autofunction:: to_dash_case
Placeholder values
^^^^^^^^^^^^^^^^^^
These values are used in places where :data:`None` is ambiguous.
.. autodata:: DISABLED
.. autodata:: MISSING
.. autodata:: POSITIONAL
.. autodata:: Disabled
.. autodata:: Missing
.. autodata:: Positional
"""
import abc as _abc
import enum as _enum
import logging as _logging
import os as _os
import re as _re
import sys as _sys
import textwrap as _textwrap
from yuio import _t
try:
from yuio._version import __version__, __version_tuple__
except ImportError:
raise ImportError(
"yuio._version not found. if you are developing locally, "
"run `pip install -e .[test,doc]` to generate it"
)
__all__ = [
"SupportsLt",
"to_dash_case",
"Disabled",
"DISABLED",
"Missing",
"MISSING",
"Positional",
"POSITIONAL",
]
_logger = _logging.getLogger("yuio.internal")
_logger.setLevel("DEBUG") # handlers will do all the filtering
_logger.propagate = False
_debug = "YUIO_DEBUG" in _os.environ
if _debug:
__level = _os.environ.get("YUIO_DEBUG_LEVEL", "DEBUG")
__file = _os.environ.get("YUIO_DEBUG_FILE") or "yuio.log"
__file_handler = _logging.FileHandler(__file, delay=True)
__file_handler.setLevel(__level)
_logger.addHandler(__file_handler)
__stderr_handler = _logging.StreamHandler(_sys.__stderr__)
__stderr_handler.setLevel("CRITICAL")
_logger.addHandler(__stderr_handler)
_T_contra = _t.TypeVar("_T_contra", contravariant=True)
class SupportsLt(_t.Protocol[_T_contra]):
"""Protocol for objects that can be compared to each other."""
@_abc.abstractmethod
def __lt__(self, other: _T_contra, /) -> bool:
...
_TO_DASH_CASE_RE = _re.compile(
r"""
# We will add a dash (bear with me here):
_ # 1. instead of underscore,
| ( # 2. OR in the following case:
(?<!^) # - not at the beginning of the string,
( # - AND EITHER:
(?<=[A-Z])(?=[A-Z][a-z]) # - before case gets lower (`XMLTag` -> `XML-Tag`),
| (?<=[a-zA-Z])(?![a-zA-Z_]) # - between a letter and a non-letter (`HTTP20` -> `HTTP-20`),
| (?<![A-Z_])(?=[A-Z]) # - between non-uppercase and uppercase letter (`TagXML` -> `Tag-XML`),
) # - AND ALSO:
(?!$) # - not at the end of the string.
)
""",
_re.VERBOSE | _re.MULTILINE,
)
def to_dash_case(s: str, /) -> str:
"""Convert ``CamelCase`` or ``snake_case`` identifier to a ``dash-case`` one.
This function assumes ASCII input, and will not work correctly
with non-ASCII characters.
"""
return _TO_DASH_CASE_RE.sub("-", s).lower()
_COMMENT_RE = _re.compile(r"^\s*#: ?(.*)\r?\n?$")
def _find_docs(obj: _t.Any) -> _t.Dict[str, str]:
# based on code from Sphinx
import ast
import inspect
import itertools
if "<locals>" in obj.__qualname__:
# This will not work as expected!
return {}
sourcelines, _ = inspect.getsourcelines(obj)
docs: _t.Dict[str, str] = {}
node = ast.parse(_textwrap.dedent("".join(sourcelines)))
assert isinstance(node, ast.Module)
assert len(node.body) == 1
cdef = node.body[0]
if isinstance(cdef, ast.ClassDef):
fields = [
(stmt.lineno, stmt.target.id)
for stmt in cdef.body
if (
isinstance(stmt, ast.AnnAssign)
and isinstance(stmt.target, ast.Name)
and not stmt.target.id.startswith("_")
)
]
elif isinstance(cdef, ast.FunctionDef):
fields = [
(field.lineno, field.arg)
for field in itertools.chain(cdef.args.args, cdef.args.kwonlyargs)
]
else:
return {}
for pos, name in fields:
comment_lines: _t.List[str] = []
for before_line in sourcelines[pos - 2 :: -1]:
if match := _COMMENT_RE.match(before_line):
comment_lines.append(_textwrap.dedent(match.group(1)))
else:
break
if comment_lines:
docs[name] = "\n".join(reversed(comment_lines))
return docs
def _commonprefix(m: _t.List[str]) -> str:
if not m:
return ""
s1 = min(m)
s2 = max(m)
for i, c in enumerate(s1):
if c != s2[i]:
return s1[:i]
return s1
def _with_slots() -> _t.Dict[_t.Literal["slots"], bool]:
return {} if _sys.version_info < (3, 11) else {"slots": True}
class _Placeholders(_enum.Enum):
DISABLED = "<disabled>"
MISSING = "<missing>"
POSITIONAL = "<positional>"
def __repr__(self):
return self.value
#: Type of the :data:`DISABLED` placeholder.
Disabled: _t.TypeAlias = _t.Literal[_Placeholders.DISABLED]
#: Indicates that some functionality is disabled.
DISABLED: Disabled = _Placeholders.DISABLED
#: Type of the :data:`MISSING` placeholder.
Missing: _t.TypeAlias = _t.Literal[_Placeholders.MISSING]
#: Indicates that some value is missing.
MISSING: Missing = _Placeholders.MISSING
#: Type of the :data:`POSITIONAL` placeholder.
Positional: _t.TypeAlias = _t.Literal[_Placeholders.POSITIONAL]
#: Used with :func:`field` to enable positional arguments.
POSITIONAL: Positional = _Placeholders.POSITIONAL