diff --git a/CHANGELOG.md b/CHANGELOG.md index b8f6fc1..5e86ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## [0.5.2] 2026-07-02 +### Modified +- `lazy-loader` package is now optional. + +### Fixed +- `Placeholder` and `requires_packages` in global context. +- `as_builtin` now support conversions of dataclasses attributes, without converting to dict with `dataclasses.asdict`. + ## [0.5.1] 2026-06-19 ### Added - Argument `by` to `sorted_dict` function. diff --git a/CITATION.cff b/CITATION.cff index 8fdfeb7..655320e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -17,5 +17,5 @@ keywords: - tools - utilities license: MIT -version: 0.5.1 -date-released: '2026-06-19' +version: 0.5.2 +date-released: '2026-07-02' diff --git a/README.md b/README.md index c5596c6..db1cbc6 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,9 @@ Easely converts common python structures like list of dicts to dict of lists : ... True ``` +## Lazy loading +To speed up this package loading, you can install it with `uv add pythonwrench[lazy]`, so `import pythonwrench as pw` will be faster. + ## Contact Maintainer: - [Étienne Labbé](https://labbeti.github.io/) "Labbeti": labbeti.pub@gmail.com diff --git a/pyproject.toml b/pyproject.toml index 5c39a6b..b912377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ maintainers = [ ] dynamic = ["version"] dependencies = [ - "lazy-loader>=0.4", "typing-extensions>=4.10.0", ] diff --git a/src/pythonwrench/__init__.py b/src/pythonwrench/__init__.py index ce512f9..d248eab 100644 --- a/src/pythonwrench/__init__.py +++ b/src/pythonwrench/__init__.py @@ -9,7 +9,7 @@ __license__ = "MIT" __maintainer__ = "Étienne Labbé (Labbeti)" __status__ = "Development" -__version__ = "0.5.1" +__version__ = "0.5.2" from typing import TYPE_CHECKING @@ -114,10 +114,12 @@ ) from .hashlib import hash_file from .importlib import ( + Placeholder, is_available_package, is_editable_package, reload_editable_packages, reload_submodules, + requires_packages, search_submodules, ) from .inspect import get_argnames, get_current_fn_name, get_fullname @@ -325,7 +327,9 @@ "is_editable_package", "reload_editable_packages", "reload_submodules", + "requires_packages", "search_submodules", + "Placeholder", ], "inspect": ["get_argnames", "get_current_fn_name", "get_fullname"], "json": [ diff --git a/src/pythonwrench/argparse.py b/src/pythonwrench/argparse.py index ed960bc..7585f09 100644 --- a/src/pythonwrench/argparse.py +++ b/src/pythonwrench/argparse.py @@ -119,8 +119,6 @@ def str_to_optional_bool( x: str, *, case_sensitive: bool = False, - true_values: Union[str, Iterable[str]] = DEFAULT_TRUE_VALUES, - false_values: Union[str, Iterable[str]] = DEFAULT_FALSE_VALUES, none_values: Union[str, Iterable[str]] = DEFAULT_NONE_VALUES, ) -> Optional[bool]: """Convert string values to optional bool safely. Intended for argparse arguments. diff --git a/src/pythonwrench/cast.py b/src/pythonwrench/cast.py index 04c1c41..f78a3d0 100644 --- a/src/pythonwrench/cast.py +++ b/src/pythonwrench/cast.py @@ -3,7 +3,6 @@ from argparse import Namespace from collections import Counter -from dataclasses import asdict from datetime import date from enum import Enum from functools import partial @@ -119,7 +118,10 @@ def _namespace_to_builtin(x: Namespace, **kwargs) -> Any: @register_as_builtin_fn(DataclassInstance) def _dataclass_to_builtin(x: DataclassInstance, **kwargs) -> Any: - return as_builtin(asdict(x), **kwargs) + # IMPORTANT note : we do not use dataclasses.asdict() because it also converts attributes like Counter, but not do dicts + field_names = x.__dataclass_fields__.keys() + xdict = {name: as_builtin(getattr(x, name), **kwargs) for name in field_names} + return xdict @register_as_builtin_fn(NamedTupleInstance) diff --git a/src/pythonwrench/functools.py b/src/pythonwrench/functools.py index 50e1f90..4da1f02 100644 --- a/src/pythonwrench/functools.py +++ b/src/pythonwrench/functools.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import inspect +from types import CodeType from typing import ( Any, Callable, @@ -14,7 +16,7 @@ from typing_extensions import ParamSpec from pythonwrench._core import _decorator_factory, return_none # noqa: F401 -from pythonwrench.inspect import _get_code_and_start, get_argnames +from pythonwrench.inspect import get_argnames from pythonwrench.typing import isinstance_generic T = TypeVar("T") @@ -162,3 +164,21 @@ def identity(x: T, **kwargs) -> T: def repeat_fn(f: Callable[[T], T], n: int) -> Callable[[T], T]: """Creates wrapper which call a function n items.""" return Compose([f] * n) + + +def _get_code_and_start(fn: Callable) -> Tuple[CodeType, int]: + if inspect.isfunction(fn): + code = fn.__code__ + start = 0 + elif inspect.ismethod(fn): + code = fn.__code__ + start = 1 # If method, remove 'self' arg + elif inspect.isclass(fn): + # If init, remove 'self' arg + code = fn.__init__.__code__ + start = 1 # If init, remove 'self' arg + else: + code = fn.__call__.__code__ + start = 0 + + return code, start diff --git a/src/pythonwrench/inspect.py b/src/pythonwrench/inspect.py index 7462940..2c9d19a 100644 --- a/src/pythonwrench/inspect.py +++ b/src/pythonwrench/inspect.py @@ -2,8 +2,7 @@ # -*- coding: utf-8 -*- import inspect -from types import CodeType -from typing import Any, Callable, List, Tuple, TypeVar, Union, get_args +from typing import Any, Callable, List, TypeVar, Union, get_args T = TypeVar("T") @@ -68,21 +67,3 @@ def get_fullname(x: Any, *, inst_suffix: str = "(...)") -> str: name = f"{name}[{argsnames_str}]" return name - - -def _get_code_and_start(fn: Callable) -> Tuple[CodeType, int]: - if inspect.isfunction(fn): - code = fn.__code__ - start = 0 - elif inspect.ismethod(fn): - code = fn.__code__ - start = 1 # If method, remove 'self' arg - elif inspect.isclass(fn): - # If init, remove 'self' arg - code = fn.__init__.__code__ - start = 1 # If init, remove 'self' arg - else: - code = fn.__call__.__code__ - start = 0 - - return code, start diff --git a/tests/test_cast.py b/tests/test_cast.py index 09987cd..102c480 100644 --- a/tests/test_cast.py +++ b/tests/test_cast.py @@ -3,6 +3,7 @@ import unittest from collections import Counter +from dataclasses import dataclass, field from pathlib import Path from unittest import TestCase @@ -14,8 +15,17 @@ class CustomClass: b: str = "" +@dataclass +class CustomDataclass: + counts: Counter = field(default_factory=Counter) + + class TestCast(TestCase): def test_example_1(self) -> None: + @register_as_builtin_fn(CustomClass) + def customclass_to_builtin(x: CustomClass) -> dict: + return {"a": x.a, "b": x.b, "added_prop": None} + examples = [ ("a", "a"), ((), []), @@ -25,12 +35,10 @@ def test_example_1(self) -> None: (CustomClass(), {"a": 0, "b": "", "added_prop": None}), ({"a": (1, 2)}, {"a": [1, 2]}), (Counter(a=2, b=1, c=3), {"a": 2, "b": 1, "c": 3}), + (Counter(["a", "b", "a"]), {"a": 2, "b": 1}), + (CustomDataclass(Counter(["a", "b", "a"])), {"counts": {"a": 2, "b": 1}}), ] - @register_as_builtin_fn(CustomClass) - def customclass_to_builtin(x: CustomClass) -> dict: - return {"a": x.a, "b": x.b, "added_prop": None} - for x, expected in examples: result = as_builtin(x) assert result == expected diff --git a/uv.lock b/uv.lock index c5ee76e..592f132 100644 --- a/uv.lock +++ b/uv.lock @@ -2607,7 +2607,6 @@ wheels = [ name = "pythonwrench" source = { editable = "." } dependencies = [ - { name = "lazy-loader" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ]