Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ __pycache__/
__pypackages__/
.mypy_cache/
.pytest_cache/
*.py[cod]
*.py[cdio]
*$py.class

# BUILD ARTIFACTS
Expand Down
20 changes: 18 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,28 @@
# <br><b>Changelog</b><br>


<span id="v1-9-6" />

## ... `v1.9.6`

* The compiled version of the library now includes the type stub files (`.pyi`), so type checkers can properly check types.
* Made all type hints in the whole library way more strict and accurate.
* Removed leftover unnecessary runtime type-checks in several methods throughout the whole library.

**BREAKING CHANGES:**
* All methods that should use positional-only params up to a certain point, now actually enforce that by using the `/` syntax.
* Renamed the `Spinner` class from the `console` module to `Throbber`, since that name is closer to what it's actually used for.
* Changed the name of the TypeAlias `DataStructure` to `DataObj` because that name is shorter and more general.
* Changed both names `DataStructureTypes` and `IndexIterableTypes` to `DataObjTT` and `IndexIterableTT` respectively (`TT` *stands for types-tuple*).
* Made the return value of `String.single_char_repeats()` always be *`int`* and not <code>*int* | *bool*</code>.


<span id="v1-9-5" />

## 25.01.2026 `v1.9.5`

* Add new class property `Console.encoding`, which returns the encoding used by the console (*e.g.* `utf-8`*,* `cp1252`*, …*).
* Add multiple new class properties to the `System` class:
* Added a new class property `Console.encoding`, which returns the encoding used by the console (*e.g.* `utf-8`*,* `cp1252`*, …*).
* Added multiple new class properties to the `System` class:
- `is_linux` Whether the current OS is Linux or not.
- `is_mac` Whether the current OS is macOS or not.
- `is_unix` Whether the current OS is a Unix-like OS (Linux, macOS, BSD, …) or not.
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "xulbux"
version = "1.9.5"
version = "1.9.6"
description = "A Python library to simplify common programming tasks."
readme = "README.md"
authors = [{ name = "XulbuX", email = "xulbux.real@gmail.com" }]
Expand Down Expand Up @@ -130,6 +130,9 @@ package-dir = { "" = "src" }
[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
xulbux = ["py.typed", "*.pyi", "**/*.pyi"]

[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q"
Expand Down
58 changes: 56 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from setuptools import setup
from pathlib import Path
import subprocess
import sys
import os


Expand All @@ -10,14 +12,66 @@ def find_python_files(directory: str) -> list[str]:
return python_files


# OPTIONALLY USE MYPYC COMPILATION
def generate_stubs_for_package():
print("\nGenerating stub files with stubgen...\n")

try:
skip_stubgen = {
Path("src/xulbux/base/types.py"), # COMPLEX TYPE DEFINITIONS
}

src_dir = Path("src/xulbux")
generated_count = 0
skipped_count = 0

for py_file in src_dir.rglob("*.py"):
pyi_file = py_file.with_suffix(".pyi")
rel_path = py_file.relative_to(src_dir.parent)

if py_file in skip_stubgen:
pyi_file.write_text(py_file.read_text(encoding="utf-8"), encoding="utf-8")
print(f" copied {rel_path.with_suffix('.pyi')} (preserving type definitions)")
skipped_count += 1
continue

result = subprocess.run(
[sys.executable, "-m", "mypy.stubgen",
str(py_file),
"-o", "src",
"--include-private",
"--export-less"],
capture_output=True,
text=True
)

if result.returncode == 0:
print(f" generated {rel_path.with_suffix('.pyi')}")
generated_count += 1
else:
print(f" failed {rel_path}")
if result.stderr:
print(f" {result.stderr.strip()}")

print(f"\nStub generation complete. ({generated_count} generated, {skipped_count} copied)\n")

except Exception as e:
fmt_error = "\n ".join(str(e).splitlines())
print(f"[WARNING] Could not generate stubs:\n {fmt_error}\n")


ext_modules = []

# OPTIONALLY USE MYPYC COMPILATION
if os.environ.get("XULBUX_USE_MYPYC", "1") == "1":
try:
from mypyc.build import mypycify

print("\nCompiling with mypyc...\n")
source_files = find_python_files("src/xulbux")
ext_modules = mypycify(source_files)
ext_modules = mypycify(source_files, opt_level="3")
print("\nMypyc compilation complete.\n")

generate_stubs_for_package()

except (ImportError, Exception) as e:
fmt_error = "\n ".join(str(e).splitlines())
Expand Down
2 changes: 1 addition & 1 deletion src/xulbux/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__package_name__ = "xulbux"
__version__ = "1.9.5"
__version__ = "1.9.6"
__description__ = "A Python library to simplify common programming tasks."
__status__ = "Production/Stable"

Expand Down
2 changes: 1 addition & 1 deletion src/xulbux/base/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class ANSI:
"""End of an ANSI escape sequence."""

@classmethod
def seq(cls, placeholders: int = 1) -> FormattableString:
def seq(cls, placeholders: int = 1, /) -> FormattableString:
"""Generates an ANSI escape sequence with the specified number of placeholders."""
return cls.CHAR + cls.START + cls.SEP.join(["{}" for _ in range(placeholders)]) + cls.END

Expand Down
54 changes: 39 additions & 15 deletions src/xulbux/base/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,48 +27,48 @@
#
################################################## TypeAlias ##################################################

PathsList: TypeAlias = Union[list[Path], list[str], list[Path | str]]
PathsList: TypeAlias = Union[list[Path], list[str], list[Union[Path, str]]]
"""Union of all supported list types for a list of paths."""

DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
DataObj: TypeAlias = Union[list[Any], tuple[Any, ...], set[Any], frozenset[Any], dict[Any, Any]]
"""Union of supported data structures used in the `data` module."""
DataStructureTypes = (list, tuple, set, frozenset, dict)
DataObjTT = (list, tuple, set, frozenset, dict)
"""Tuple of supported data structures used in the `data` module."""

IndexIterable: TypeAlias = Union[list, tuple, set, frozenset]
IndexIterable: TypeAlias = Union[list[Any], tuple[Any, ...], set[Any], frozenset[Any]]
"""Union of all iterable types that support indexing operations."""
IndexIterableTypes = (list, tuple, set, frozenset)
IndexIterableTT = (list, tuple, set, frozenset)
"""Tuple of all iterable types that support indexing operations."""

Rgba: TypeAlias = Union[
tuple[Int_0_255, Int_0_255, Int_0_255],
tuple[Int_0_255, Int_0_255, Int_0_255, Float_0_1],
tuple[Int_0_255, Int_0_255, Int_0_255, Optional[Float_0_1]],
list[Int_0_255],
list[Union[Int_0_255, Float_0_1]],
dict[str, Union[int, float]],
list[Union[Int_0_255, Optional[Float_0_1]]],
"RgbaDict",
"rgba",
str,
]
"""Matches all supported RGBA color value formats."""
Hsla: TypeAlias = Union[
tuple[Int_0_360, Int_0_100, Int_0_100],
tuple[Int_0_360, Int_0_100, Int_0_100, Float_0_1],
tuple[Int_0_360, Int_0_100, Int_0_100, Optional[Float_0_1]],
list[Union[Int_0_360, Int_0_100]],
list[Union[Int_0_360, Int_0_100, Float_0_1]],
dict[str, Union[int, float]],
list[Union[Int_0_360, Int_0_100, Optional[Float_0_1]]],
"HslaDict",
"hsla",
str,
]
"""Matches all supported HSLA color value formats."""
Hexa: TypeAlias = Union[str, int, "hexa"]
"""Matches all supported hexadecimal color value formats."""
"""Matches all supported HEXA color value formats."""

AnyRgba: TypeAlias = Any
"""Generic type alias for RGBA color values in any supported format (type checking disabled)."""
"""Generic type alias for RGBA color values in any format (type checking disabled)."""
AnyHsla: TypeAlias = Any
"""Generic type alias for HSLA color values in any supported format (type checking disabled)."""
"""Generic type alias for HSLA color values in any format (type checking disabled)."""
AnyHexa: TypeAlias = Any
"""Generic type alias for hexadecimal color values in any supported format (type checking disabled)."""
"""Generic type alias for HEXA color values in any format (type checking disabled)."""

ArgParseConfig: TypeAlias = Union[set[str], "ArgConfigWithDefault", Literal["before", "after"]]
"""Matches the command-line-parsing configuration of a single argument."""
Expand Down Expand Up @@ -101,6 +101,30 @@ class ArgData(TypedDict):
flag: Optional[str]


class RgbaDict(TypedDict):
"""Dictionary schema for RGBA color components."""
r: Int_0_255
g: Int_0_255
b: Int_0_255
a: Optional[Float_0_1]


class HslaDict(TypedDict):
"""Dictionary schema for HSLA color components."""
h: Int_0_360
s: Int_0_100
l: Int_0_100
a: Optional[Float_0_1]


class HexaDict(TypedDict):
"""Dictionary schema for HEXA color components."""
r: str
g: str
b: str
a: Optional[str]


class MissingLibsMsgs(TypedDict):
"""Configuration schema for custom messages in `System.check_libs()` when checking library dependencies."""
found_missing: str
Expand Down
4 changes: 2 additions & 2 deletions src/xulbux/cli/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ def is_latest_version() -> Optional[bool]:


def show_help() -> None:
FormatCodes._config_console()
FormatCodes._config_console() # type: ignore[protected-access]
print(CLI_HELP)
Console.pause_exit(pause=True, prompt=" [dim](Press any key to exit...)\n\n")
Console.pause_exit(" [dim](Press any key to exit...)\n\n", pause=True)
15 changes: 8 additions & 7 deletions src/xulbux/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
from .regex import Regex
from .data import Data

from typing import Any
import regex as _rx


class Code:
"""This class includes methods to work with code strings."""

@classmethod
def add_indent(cls, code: str, indent: int) -> str:
def add_indent(cls, code: str, indent: int, /) -> str:
"""Adds `indent` spaces at the beginning of each line.\n
--------------------------------------------------------------------------
- `code` -⠀the code to indent
Expand All @@ -24,15 +25,15 @@ def add_indent(cls, code: str, indent: int) -> str:
return "\n".join(" " * indent + line for line in code.splitlines())

@classmethod
def get_tab_spaces(cls, code: str) -> int:
def get_tab_spaces(cls, code: str, /) -> int:
"""Will try to get the amount of spaces used for indentation.\n
----------------------------------------------------------------
- `code` -⠀the code to analyze"""
indents = [len(line) - len(line.lstrip()) for line in String.get_lines(code, remove_empty_lines=True)]
return min(non_zero_indents) if (non_zero_indents := [i for i in indents if i > 0]) else 0

@classmethod
def change_tab_size(cls, code: str, new_tab_size: int, remove_empty_lines: bool = False) -> str:
def change_tab_size(cls, code: str, new_tab_size: int, /, *, remove_empty_lines: bool = False) -> str:
"""Replaces all tabs with `new_tab_size` spaces.\n
--------------------------------------------------------------------------------
- `code` -⠀the code to modify the tab size of
Expand All @@ -48,19 +49,19 @@ def change_tab_size(cls, code: str, new_tab_size: int, remove_empty_lines: bool
return "\n".join(code_lines)
return code

result = []
result: list[str] = []
for line in code_lines:
indent_level = (len(line) - len(stripped := line.lstrip())) // tab_spaces
result.append((" " * (indent_level * new_tab_size)) + stripped)

return "\n".join(result)

@classmethod
def get_func_calls(cls, code: str) -> list:
def get_func_calls(cls, code: str, /) -> list[list[Any]]:
"""Will try to get all function calls and return them as a list.\n
-------------------------------------------------------------------
- `code` -⠀the code to analyze"""
nested_func_calls = []
nested_func_calls: list[list[Any]] = []

for _, func_attrs in (funcs := _rx.findall(r"(?i)" + Regex.func_call(), code)):
if (nested_calls := _rx.findall(r"(?i)" + Regex.func_call(), func_attrs)):
Expand All @@ -69,7 +70,7 @@ def get_func_calls(cls, code: str) -> list:
return list(Data.remove_duplicates(funcs + nested_func_calls))

@classmethod
def is_js(cls, code: str, funcs: set[str] = {"__", "$t", "$lang"}) -> bool:
def is_js(cls, code: str, /, *, funcs: set[str] = {"__", "$t", "$lang"}) -> bool:
"""Will check if the code is very likely to be JavaScript.\n
-------------------------------------------------------------
- `code` -⠀the code to analyze
Expand Down
Loading
Loading