Skip to content
Merged
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
6 changes: 5 additions & 1 deletion .github/workflows/test-and-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
pip install flake8 flake8-pyproject pytest
pip install flake8 flake8-pyproject pyright pytest

- name: Lint with flake8
run: |
python -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
python -m flake8 . --exit-zero --max-complexity=12 --statistics

- name: Type check with pyright
run: |
pyright

- name: Test with pytest
run: |
python -m pytest --verbose
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
32 changes: 24 additions & 8 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" />

## 13.04.2026 `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 and/or keyword-only params, now actually enforce that by using the `/` and `*` syntax in the method definitions.
* 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 type-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 Expand Up @@ -482,10 +498,10 @@
## 21.12.2024 `v1.5.9`

* Fixed bugs in method `to_ansi()` in module `xx_format_codes`:<br>
1. The method always returned an empty string, because the color validation was broken, and it would identify all colors as invalid.<br>
Now the validation `Color.is_valid_rgba()` and `Color.is_valid_hexa()` are fixed and now, if a color is identified as invalid, the method returns the original string instead of an empty string.
2. Previously the method `to_ansi()` couldn't handle formats inside `[]` because everything inside the brackets was recognized as an invalid format.<br>
Now you are able to use formats inside `[]` (*e.g.* `"[[red](Red text [b](inside) square brackets!)]"`).
1. The method always returned an empty string, because the color validation was broken, and it would identify all colors as invalid.<br>
Now the validation `Color.is_valid_rgba()` and `Color.is_valid_hexa()` are fixed and now, if a color is identified as invalid, the method returns the original string instead of an empty string.
2. Previously the method `to_ansi()` couldn't handle formats inside `[]` because everything inside the brackets was recognized as an invalid format.<br>
Now you are able to use formats inside `[]` (*e.g.* `"[[red](Red text [b](inside) square brackets!)]"`).
* Introduced a new test for the `xx_format_codes` module.
* Fixed a small bug in the help client-command:<br>
Added back the default text color.
Expand Down Expand Up @@ -849,8 +865,8 @@
## 15.10.2024 `v1.0.1` `v1.0.2` `v1.0.3` `v1.0.4` `v1.0.5`

* Fixed `f-string` issues for Python 3.10:
1. Not making use of same quotes inside f-strings any more.
2. No backslash escaping in f-strings.
1. Not making use of same quotes inside f-strings any more.
2. No backslash escaping in f-strings.


<span id="release" /><span id="v1-0-0" />
Expand Down
14 changes: 11 additions & 3 deletions 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 All @@ -30,6 +30,7 @@ dependencies = [
optional-dependencies = { dev = [
"flake8-pyproject>=1.2.3",
"flake8>=6.1.0",
"pyright>=1.1.408",
"pytest>=7.4.2",
"toml>=0.10.2",
] }
Expand Down Expand Up @@ -121,15 +122,18 @@ xulbux-help = "xulbux.cli.help:show_help"
max-complexity = 12
max-line-length = 127
select = ["E", "F", "W", "C90"]
extend-ignore = ["E203", "E266", "E502", "W503"]
per-file-ignores = ["__init__.py:F403,F405"]
extend-ignore = ["E124", "E203", "E266", "E502", "W503"]
per-file-ignores = ["__init__.py:F403,F405", "types.py:E302,E305"]

[tool.setuptools]
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 All @@ -150,3 +154,7 @@ testpaths = [
"tests/test_string.py",
"tests/test_system.py",
]

[tool.pyright]
include = ["src", "tests"]
typeCheckingMode = "strict"
64 changes: 62 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from setuptools import setup
from pathlib import Path
import subprocess
import shutil
import sys
import os


Expand All @@ -10,14 +13,71 @@ 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
Path("src/xulbux/__init__.py"), # PRESERVE PACKAGE METADATA CONSTANTS
}

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

stubgen_exe = (
shutil.which("stubgen")
or str(Path(sys.executable).parent / ("stubgen.exe" if sys.platform == "win32" else "stubgen"))
)
result = subprocess.run(
[stubgen_exe,
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
4 changes: 1 addition & 3 deletions src/xulbux/base/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

from .decorators import mypyc_attr

#
################################################## FILE ##################################################

################################################## FILE ##################################################

@mypyc_attr(native_class=False)
class SameContentFileExistsError(FileExistsError):
Expand All @@ -16,7 +15,6 @@ class SameContentFileExistsError(FileExistsError):

################################################## PATH ##################################################


@mypyc_attr(native_class=False)
class PathNotFoundError(FileNotFoundError):
"""Raised when a file system path does not exist or cannot be accessed."""
Expand Down
Loading
Loading