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
20 changes: 17 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,24 @@
# <br><b>Changelog</b><br>


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

## ... `v1.9.0` Big Update 🚀
* Standardized the docstrings for all public methods in the whole library to use the same style and structure.
* Replaced left over single quotes with double quotes for consistency.
* Fixed a bug inside `Data.remove_empty_items()`, where types other than strings where passed to `String.is_empty()`, which caused an exception.
* Refactored/reformatted the code of the whole library, to introduce more clear code structure with more room to breathe.
* Made the really complex regex patterns in the `Regex` class all multi-line for better readability.
* Added a new internal method `Regex._clean()`, which is used to clean up the regex patterns, defined as multi-line strings.
* Moved custom exception classes to their own file `base/exceptions.py`, so the user can easily import them all from the same place.
* Moved custom types to their own file `base/types.py`, so the user can easily import them all from the same place.
* Removed unnecessary duplicate code in several methods throughout the library.
* Introduced some minor performance improvements in a few methods, that might be called very often in a short time span.
* Added a small description to the docstrings of all modules and their main classes.

## ... `v1.8.6`
* Standardized the docstrings for all public methods in the whole library to use the same style and format.
**BREAKING CHANGES:**
* The `find_args` param from the method `Console.get_args()` now only accepts sets for the flags instead of lists/tuples, since the order of flags doesn't matter and sets have better performance for lookups.
* Added missing type checking to all public methods in the whole library, so now they will all throw errors if the params aren't of the expected type.


<span id="v1-8-5" />
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# **xulbux**

[![](https://img.shields.io/pypi/v/xulbux?style=flat&labelColor=404560&color=7075FF)](https://pypi.org/project/xulbux) [![](https://img.shields.io/pepy/dt/xulbux?style=flat&labelColor=404560&color=7075FF)](https://clickpy.clickhouse.com/dashboard/xulbux) [![](https://img.shields.io/github/license/XulbuX/PythonLibraryXulbuX?style=flat&labelColor=405555&color=70FFEE)](https://github.com/XulbuX/PythonLibraryXulbuX/blob/main/LICENSE) [![](https://img.shields.io/github/last-commit/XulbuX/PythonLibraryXulbuX?style=flat&labelColor=554045&color=FF6065)](https://github.com/XulbuX/PythonLibraryXulbuX/commits) [![](https://img.shields.io/github/issues/XulbuX/PythonLibraryXulbuX?style=flat&labelColor=554045&color=FF6065)](https://github.com/XulbuX/PythonLibraryXulbuX/issues) [![](https://img.shields.io/github/stars/XulbuX/PythonLibraryXulbuX?label=★&style=flat&labelColor=604A40&color=FF9673)
](https://github.com/XulbuX/PythonLibraryXulbuX/stargazers)
[![](https://img.shields.io/pypi/v/xulbux?style=flat&labelColor=404560&color=7075FF)](https://pypi.org/project/xulbux) [![](https://img.shields.io/pepy/dt/xulbux?style=flat&labelColor=404560&color=7075FF)](https://clickpy.clickhouse.com/dashboard/xulbux) [![](https://img.shields.io/github/license/XulbuX/PythonLibraryXulbuX?style=flat&labelColor=405555&color=70FFEE)](https://github.com/XulbuX/PythonLibraryXulbuX/blob/main/LICENSE) [![](https://img.shields.io/github/last-commit/XulbuX/PythonLibraryXulbuX?style=flat&labelColor=554045&color=FF6065)](https://github.com/XulbuX/PythonLibraryXulbuX/commits) [![](https://img.shields.io/github/issues/XulbuX/PythonLibraryXulbuX?style=flat&labelColor=554045&color=FF6065)](https://github.com/XulbuX/PythonLibraryXulbuX/issues) [![](https://img.shields.io/github/stars/XulbuX/PythonLibraryXulbuX?label=★&style=flat&labelColor=604A40&color=FF9673)](https://github.com/XulbuX/PythonLibraryXulbuX/stargazers)

**`xulbux`** is a library that contains many useful classes, types, and functions,
ranging from console logging and working with colors to file management and system operations.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "xulbux"
version = "1.8.5"
version = "1.9.0"
authors = [{ name = "XulbuX", email = "[email protected]" }]
maintainers = [{ name = "XulbuX", email = "[email protected]" }]
description = "A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler."
Expand Down
2 changes: 1 addition & 1 deletion src/xulbux/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.8.5"
__version__ = "1.9.0"

__author__ = "XulbuX"
__email__ = "[email protected]"
Expand Down
14 changes: 14 additions & 0 deletions src/xulbux/base/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
################################################## FILE ##################################################


class SameContentFileExistsError(FileExistsError):
"""Exception raised when a file with the same name and content already exists."""
...


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


class PathNotFoundError(FileNotFoundError):
"""Exception raised when a specified path could not be found."""
...
81 changes: 81 additions & 0 deletions src/xulbux/base/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import TYPE_CHECKING, Annotated, TypeAlias, TypedDict, Optional, Union, Any
import regex as _rx
import re as _re

# PREVENT CIRCULAR IMPORTS
if TYPE_CHECKING:
from ..color import rgba, hsla, hexa

#
################################################## COLOR ##################################################

Int_0_100 = Annotated[int, "An integer in range [0, 100]."]
Int_0_255 = Annotated[int, "An integer in range [0, 255]."]
Int_0_360 = Annotated[int, "An integer in range [0, 360]."]
Float_0_1 = Annotated[float, "A float in range [0.0, 1.0]."]

AnyRgba: TypeAlias = Any
AnyHsla: TypeAlias = Any
AnyHexa: TypeAlias = Any

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],
list[Int_0_255],
list[Union[Int_0_255, Float_0_1]],
dict[str, Union[int, float]],
"rgba",
str,
]
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],
list[Union[Int_0_360, Int_0_100]],
list[Union[Int_0_360, Int_0_100, Float_0_1]],
dict[str, Union[int, float]],
"hsla",
str,
]
Hexa: TypeAlias = Union[str, int, "hexa"]

#
################################################## CONSOLE ##################################################


class ArgConfigWithDefault(TypedDict):
"""TypedDict for flagged argument configuration with default value."""
flags: set[str]
default: str


class ArgResultRegular(TypedDict):
"""TypedDict for regular flagged argument results."""
exists: bool
value: Optional[str]


class ArgResultPositional(TypedDict):
"""TypedDict for positional `"before"`/`"after"` argument results."""
exists: bool
values: list[str]


################################################## DATA ##################################################

DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
IndexIterable: TypeAlias = Union[list, tuple, set, frozenset]

#
################################################## REGEX ##################################################

Pattern: TypeAlias = _re.Pattern[str] | _rx.Pattern[str]
Match: TypeAlias = _re.Match[str] | _rx.Match[str]

#
################################################## SYSTEM ##################################################


class MissingLibsMsgs(TypedDict):
"""TypedDict for the `missing_libs_msgs` parameter in `System.check_libs()`."""
found_missing: str
should_install: str
4 changes: 2 additions & 2 deletions src/xulbux/cli/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def get_latest_version() -> Optional[str]:

def is_latest_version() -> Optional[bool]:
try:
if (latest := get_latest_version()) in ("", None):
if (latest := get_latest_version()) in {"", None}:
return None
latest_v_parts = tuple(int(part) for part in latest.lower().lstrip("v").split('.'))
latest_v_parts = tuple(int(part) for part in (latest or "").lower().lstrip("v").split('.'))
installed_v_parts = tuple(int(part) for part in __version__.lower().lstrip("v").split('.'))
return latest_v_parts <= installed_v_parts
except Exception:
Expand Down
111 changes: 77 additions & 34 deletions src/xulbux/code.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
This module provides the `Code` class, which offers methods to work with code strings.
"""

from .string import String
from .regex import Regex
from .data import Data
Expand All @@ -6,62 +10,99 @@


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

@staticmethod
def add_indent(code: str, indent: int) -> str:
"""Adds `indent` spaces at the beginning of each line."""
indented_lines = [" " * indent + line for line in code.splitlines()]
return "\n".join(indented_lines)
"""Adds `indent` spaces at the beginning of each line.\n
--------------------------------------------------------------------------
- `code` -⠀the code to indent
- `indent` -⠀the amount of spaces to add at the beginning of each line"""
if not isinstance(code, str):
raise TypeError(f"The 'code' parameter must be a string, got {type(code)}")
if not isinstance(indent, int):
raise TypeError(f"The 'indent' parameter must be an integer, got {type(indent)}")
elif indent < 0:
raise ValueError(f"The 'indent' parameter must be non-negative, got {indent!r}")

return "\n".join(" " * indent + line for line in code.splitlines())

@staticmethod
def get_tab_spaces(code: str) -> int:
"""Will try to get the amount of spaces used for indentation."""
code_lines = String.get_lines(code, remove_empty_lines=True)
indents = [len(line) - len(line.lstrip()) for line in code_lines]
non_zero_indents = [i for i in indents if i > 0]
return min(non_zero_indents) if non_zero_indents else 0
"""Will try to get the amount of spaces used for indentation.\n
----------------------------------------------------------------
- `code` -⠀the code to analyze"""
if not isinstance(code, str):
raise TypeError(f"The 'code' parameter must be a string, got {type(code)}")

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

@staticmethod
def change_tab_size(code: str, new_tab_size: int, remove_empty_lines: bool = False) -> str:
"""Replaces all tabs with `new_tab_size` spaces.\n
----------------------------------------------------------------------------------
If `remove_empty_lines` is `True`, empty lines will be removed in the process."""
code_lines = String.get_lines(code, remove_empty_lines=True)
lines = code_lines if remove_empty_lines else String.get_lines(code)
tab_spaces = Code.get_tab_spaces(code)
if (tab_spaces == new_tab_size) or tab_spaces == 0:
--------------------------------------------------------------------------------
- `code` -⠀the code to modify the tab size of
- `new_tab_size` -⠀the new amount of spaces per tab
- `remove_empty_lines` -⠀is true, empty lines will be removed in the process"""
if not isinstance(code, str):
raise TypeError(f"The 'code' parameter must be a string, got {type(code)}")
if not isinstance(new_tab_size, int):
raise TypeError(f"The 'new_tab_size' parameter must be an integer, got {type(new_tab_size)}")
elif new_tab_size < 0:
raise ValueError(f"The 'new_tab_size' parameter must be non-negative, got {new_tab_size!r}")
if not isinstance(remove_empty_lines, bool):
raise TypeError(f"The 'remove_empty_lines' parameter must be a boolean, got {type(remove_empty_lines)}")

code_lines = String.get_lines(code, remove_empty_lines=remove_empty_lines)

if ((tab_spaces := Code.get_tab_spaces(code)) == new_tab_size) or tab_spaces == 0:
if remove_empty_lines:
return "\n".join(code_lines)
return code

result = []
for line in lines:
stripped = line.lstrip()
indent_level = (len(line) - len(stripped)) // tab_spaces
new_indent = " " * (indent_level * new_tab_size)
result.append(new_indent + stripped)
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)

@staticmethod
def get_func_calls(code: str) -> list:
"""Will try to get all function calls and return them as a list."""
funcs = _rx.findall(r"(?i)" + Regex.func_call(), code)
"""Will try to get all function calls and return them as a list.\n
-------------------------------------------------------------------
- `code` -⠀the code to analyze"""
if not isinstance(code, str):
raise TypeError(f"The 'code' parameter must be a string, got {type(code)}")

nested_func_calls = []
for _, func_attrs in funcs:
nested_calls = _rx.findall(r"(?i)" + Regex.func_call(), func_attrs)
if nested_calls:

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)):
nested_func_calls.extend(nested_calls)

return list(Data.remove_duplicates(funcs + nested_func_calls))

@staticmethod
def is_js(code: str, funcs: list[str] = ["__", "$t", "$lang"]) -> bool:
"""Will check if the code is very likely to be JavaScript."""
if not code or len(code.strip()) < 3:
def is_js(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
- `funcs` -⠀a list of custom function names to check for"""
if not isinstance(code, str):
raise TypeError(f"The 'code' parameter must be a string, got {type(code)}")
elif len(code.strip()) < 3:
return False
if not isinstance(funcs, set):
raise TypeError(f"The 'funcs' parameter must be a set, got {type(funcs)}")

for func in funcs:
if _rx.match(r"^[\s\n]*" + _rx.escape(func) + r"\([^\)]*\)[\s\n]*$", code):
return True

direct_js_patterns = [
r"^[\s\n]*\$\(['\"][^'\"]+['\"]\)\.[\w]+\([^\)]*\);?[\s\n]*$", # jQuery calls
r"""^[\s\n]*\$\(["'][^"']+["']\)\.[\w]+\([^\)]*\);?[\s\n]*$""", # jQuery calls
r"^[\s\n]*\$\.[a-zA-Z]\w*\([^\)]*\);?[\s\n]*$", # $.ajax(), etc.
r"^[\s\n]*\(\s*function\s*\(\)\s*\{.*\}\s*\)\(\);?[\s\n]*$", # IIFE
r"^[\s\n]*document\.[a-zA-Z]\w*\([^\)]*\);?[\s\n]*$", # document.getElementById()
Expand All @@ -71,6 +112,7 @@ def is_js(code: str, funcs: list[str] = ["__", "$t", "$lang"]) -> bool:
for pattern in direct_js_patterns:
if _rx.match(pattern, code):
return True

arrow_function_patterns = [
r"^[\s\n]*\b[\w_]+\s*=\s*\([^\)]*\)\s*=>\s*[^;{]*[;]?[\s\n]*$", # const x = (y) => y*2;
r"^[\s\n]*\b[\w_]+\s*=\s*[\w_]+\s*=>\s*[^;{]*[;]?[\s\n]*$", # const x = y => y*2;
Expand All @@ -80,6 +122,8 @@ def is_js(code: str, funcs: list[str] = ["__", "$t", "$lang"]) -> bool:
for pattern in arrow_function_patterns:
if _rx.match(pattern, code):
return True

js_score = 0
funcs_pattern = r"(" + "|".join(_rx.escape(f) for f in funcs) + r")" + Regex.brackets("()")
js_indicators = [(r"\b(var|let|const)\s+[\w_$]+", 2), # JS variable declarations
(r"\$[\w_$]+\s*=", 2), # jQuery-style variables
Expand All @@ -98,18 +142,17 @@ def is_js(code: str, funcs: list[str] = ["__", "$t", "$lang"]) -> bool:
(r"\btry\s*\{[^}]*\}\s*catch\s*\(", 1.5), # Try-catch
(r";[\s\n]*$", 0.5), # Semicolon line endings
]
js_score = 0

line_endings = [line.strip() for line in code.splitlines() if line.strip()]
semicolon_endings = sum(1 for line in line_endings if line.endswith(';'))
if semicolon_endings >= 1:
if (semicolon_endings := sum(1 for line in line_endings if line.endswith(";"))) >= 1:
js_score += min(semicolon_endings, 2)
opening_braces = code.count('{')
closing_braces = code.count('}')
if opening_braces > 0 and opening_braces == closing_braces:
if (opening_braces := code.count("{")) > 0 and opening_braces == code.count("}"):
js_score += 1

for pattern, score in js_indicators:
regex = _rx.compile(pattern, _rx.IGNORECASE)
matches = regex.findall(code)
if matches:
js_score += len(matches) * score

return js_score >= 2
Loading